Unit tests
A simple library for unit testing in PHP 8.
A test is a class, a test is a method on it, and what a test needs is asked for in the constructor — the container builds it, the same way it builds everything else. Coverage comes with it, and needs no extension.
Requirements
- PHP 8.1 or newer
- phpdbg for coverage, which ships with PHP
Installation
composer require --dev quillstack/unit-testsUsage
A test
namespace App\Tests\Unit;
use Quillstack\UnitTests\AssertEqual;
class TestBasket
{
public function __construct(private AssertEqual $assertEqual)
{
}
public function anEmptyBasketCostsNothing()
{
$this->assertEqual->equal(0, (new Basket())->total());
}
}Every public method is a test, apart from the constructor. There is no test prefix to remember and no annotation to add: a method called anEmptyBasketCostsNothing says what it is about, and that is what the runner prints when it fails.
Whatever the constructor asks for is built for it, so a test can take the thing it is testing as well as the assertions it needs.
Listing them
tests/unit.php returns the classes to run:
return [
\App\Tests\Unit\TestBasket::class,
\App\Tests\Unit\TestCheckout::class,
];It is a PHP file rather than a config format, so a test needing something that is not there can simply be left out:
$tests = [\App\Tests\Unit\TestBasket::class];
if (getenv('DATABASE_DSN')) {
$tests[] = \App\Tests\Integration\TestOrders::class;
}
return $tests;Leaving it out is better than passing quietly: a suite which never reached the database should not look like one that did.
Running
vendor/bin/unit-testsTests: 91, passed: 91, failed: 0With coverage, under phpdbg:
phpdbg -qrr vendor/bin/unit-testsCoverage: 97.1% (813/837 lines in 34 files)
Tests: 91, passed: 91, failed: 0A file no test ever loaded still counts, uncovered — so the number says how much of the package is tested rather than how much of what ran was tested.
Assertions
Each is a class, asked for in the constructor.
| Class | Methods |
|---|---|
AssertEqual | equal() |
AssertEmpty | isEmpty(), isNotEmpty() |
AssertExceptions | expect(), expectMessage() |
Types\AssertArray | count(), isArray(), hasKey(), doesntHaveKey(), equal(), notEqual() |
Types\AssertBoolean | isTrue(), isFalse(), isBoolean() |
Types\AssertNull | isNull(), isNotNull() |
Types\AssertNumeric | isNumeric(), isInt(), isFloat() |
Types\AssertObject | instanceOf(), notNull() |
Types\AssertString | equal(), isString(), isNotString() |
Expecting an exception
expect() says what should be thrown before the thing that throws it:
public function anUnknownRuleSaysSo()
{
$this->assertExceptions->expect(UnknownRuleException::class);
$this->validator->findErrors(['a' => 1], ['a' => ['nonsense']]);
}The test fails if nothing is thrown, and if something else is.
The same test with different data
use Quillstack\UnitTests\Attributes\ProvidesDataFrom;
use Quillstack\UnitTests\DataProviderInterface;
class Prices implements DataProviderInterface
{
public function provides(): array
{
return [[1, 100], [2, 200], [3, 300]];
}
}#[ProvidesDataFrom(Prices::class)]
public function eachItemCostsAHundred(int $items, int $total)
{
$this->assertEqual->equal($total, (new Basket())->add($items)->total());
}One row per run, each holding the arguments.
Technical documentation
| Class | What it is |
|---|---|
UnitTests | the runner |
TestResult | what passed, what failed, and why |
DataProviderInterface | provides(): array — a row per run |
Attributes\ProvidesDataFrom | says which provider a test takes its rows from |
Coverage is quillstack/test-coverage, and the report it writes to unit-tests.coverage.xml is the one SonarCloud reads.
The runner works out where the project is from the working directory, so it runs from the root of a package whether or not it is installed in a plain vendor/ — a symlinked checkout used to break it.
Unit tests
This package is tested with itself:
composer test
composer test:coverage
composer stanDocker
docker-compose up -d
docker exec -w /var/www/html -it quillstack_unit-tests shLicense
MIT. See LICENSE.