mageekguy/atoum
atoum is a modern, standalone PHP unit testing framework with an expressive assertion API, built-in mocking, code coverage, and clear reports. Its simple, intuitive DSL and fluent syntax help developers write and run tests quickly, making PHP testing more enjoyable, readable, and less verbose.
atoum is a unit-testing framework with a fluent, object-based assertion API ($this->string($x)->isEqualTo(...)), first-class mock generation via a magic \mock\ namespace, and per-file process isolation. Tests are plain classes extending atoum\test; methods prefixed with test are discovered and run.
Install the Composer package (published as atoum/atoum, repo/namespace mageekguy/atoum):
composer require --dev atoum/atoum
Minimum viable setup — a tests/units directory and a test whose file/namespace/class line up:
<?php
// tests/units/Calculator.php
namespace tests\units;
use mageekguy\atoum;
class Calculator extends atoum\test
{
public function testAdd()
{
$this->integer(2 + 2)->isEqualTo(4);
}
}
Run it:
./vendor/bin/atoum -d tests/units # whole directory
./vendor/bin/atoum -f tests/units/Calculator.php # one file while iterating
For anything beyond a smoke test, add a root .atoum.php so ./vendor/bin/atoum needs no arguments:
<?php
$runner->addTestsFromDirectory(__DIR__ . '/tests/units');
$runner->setBootstrapFile(__DIR__ . '/vendor/autoload.php');
Where to look first: .atoum.php (runner/bootstrap/coverage config), the base class vendor/atoum/atoum/classes/test.php, and the classes/asserters/ directory to see every available asserter method. The docs at atoum.org are useful but stale; the source is the source of truth.
First real use case: characterization-test an existing service. Instantiate it with newTestedInstance(), mock one collaborator, call a method, and assert the return value and the collaborator interaction. That gives you a safety net before refactoring and exercises the three features you'll use daily: asserters, mocks, and testedInstance.
Tested instance instead of manual new. atoum derives the class under test from the test's namespace (tests\units\Foo → Foo) and gives you a property:
$this->newTestedInstance($mailer, $clock);
$this->testedInstance->sendInvoice($invoice);
$this->boolean($this->testedInstance->isSent())->isTrue();
Mocks with call assertions. Generate a mock from a concrete class, interface, or abstract class, stub methods through the mock controller, then verify interactions with the mock asserter:
$mailer = new \mock\MailerInterface();
$mailer->getMockController()->send = true;
$this->newTestedInstance($mailer);
$this->testedInstance->sendInvoice($invoice);
$this->mock($mailer)
->call('send')
->withArguments($invoice)
->once();
For dynamic class names use $this->newMockInstance($class). Stub before the code under test runs, otherwise the real path is taken.
Exception assertions. Wrap the throwing code in a closure and assert on the exception object:
$this
->exception(function () use ($service) {
$service->withdraw(10);
})
->isInstanceOf(\RuntimeException::class)
->hasMessage('Insufficient funds');
Deterministic time/random. atoum can intercept native function calls from the tested code, which is far cleaner than injecting a clock for a one-line time() call:
$this->function->time = function () { return 1_600_000_000; };
(Supported in current 3.x/4.x; only covers calls made in the same process as the test.)
Data providers. Annotate the test and return an array of argument arrays from a provider method — useful for table-driven validation tests. If your pinned version predates @dataProvider support, loop over the cases inside one test instead.
Hooks. beforeTestMethod($method) / afterTestMethod($method) are your setUp/tearDown. atoum creates a fresh test object per method, so instance properties are effectively per-test — but statics and singletons are not reset; clear them in the hooks.
Shared base test class. Put custom asserters and fixture helpers in tests\units\base extends atoum\test, then have suites extend it. Custom assertion methods (e.g. assertValidEmail()) compose existing asserters and keep tests readable.
CI integration. Emit a JUnit/XML report field (or use the reports extension) so your CI can ingest results, add a Composer script ("test": "atoum -d tests/units"), and use -f plus -v for local triage. Tags/annotations let you exclude slow or integration suites from the default run.
atoum/atoum while the GitHub repo and PHP namespace remain mageekguy\atoum. Don't let a composer require mageekguy/atoum failure derail setup.tests\units\ (and your src\) in composer.json autoload/autoload-dev, and keep one class per file.test* method that asserts nothing is reported as "void" — historically it does not fail the run. Make void methods fatal in runner config so a half-written test can't masquerade as green.$mock->getMockController()->sned = ... does nothing; the method returns null and your test passes for the wrong reason. Always pair stubs with a $this->mock(...)->call(...) assertion on the methods you care about.var_dump output can interleave, and debugger breakpoints must target child processes..atoum.php is picked up from the working directory and from tested directories; if both call addTestsFromDirectory, tests run twice. Keep one source of truth.xdebug.mode=coverage; coverage is slow and must be enabled in the report configuration, not just by running the binary../vendor/bin/atoum -f path/to/Test.php -v, then attach a debugger to the spawned child or temporarily run the logic from a small script. Reading the full asserter failure output (expected vs. actual, call arguments) usually beats stepping through..atoum.php are the supported seams; the atoum/bdd-extension gives a given/when/then DSL if your team prefers that style.How can I help you explore Laravel packages today?