If this is not you, one of us is in the wrong room
This talk is
Focused on PHPUnit 12/13
About core PHPUnit only, no extensions
Intended to teach you at least one new thing
Odds and ends
Custom cache directory
testssrc
Custom cache directory
testssrc
Example code
class Rectangle implements TwoDShape {
public float $area { get => $this->height * $this->width; }
public function __construct(public int $height, public int $width) {}
}
class RectangleTest extends TestCase {
public function testAreaOf2x2is4(): void {
$r = new Rectangle(2, 2);
self::assertSame(4.0, $r->area);
}
}
#[Test] attribute
class RectangleTest extends TestCase {
#[Test]
public function areaOf2x2is4(): void {
$r = new Rectangle(2, 2);
self::assertSame(4.0, $r->area);
}
}
Test sizing
#[Small]
class RectangleTest extends TestCase {
#[Test]
public function areaOf2x2is4(): void {
$r = new Rectangle(2, 2);
self::assertSame(4.0, $r->area);
}
}
Also #[Medium] and #[Large]
Small ~ Unit, Large ~ Integration
You define the line for your project
Multiplicative tests
Redundant tests :-(
#[Small]
class RectangleTest extends TestCase {
#[Test]
public function areaOf2x2is4(): void {
$r = new Rectangle(2, 2);
self::assertSame(4.0, $r->area);
}
#[Test]
public function areaOf3x2is6(): void {
$r = new Rectangle(3, 2);
self::assertSame(6.0, $r->area);
}
#[Test]
public function areaOf2x3is6(): void {
$r = new Rectangle(2, 3);
self::assertSame(6.0, $r->area);
}
}
#[TestWith]
#[Small]
class RectangleTest extends TestCase {
#[Test]
#[TestWith([2, 2, 4], '2x2=4')]
#[TestWith([3, 2, 6], '3x2=6')]
#[TestWith([2, 3, 6], '2x3=6')]
public function area(int $height, int $width, float $expectedArea): void {
$r = new Rectangle($height, $width);
self::assertSame($expectedArea, $r->area);
}
}
Data providers
#[Small]
class RectangleTest extends TestCase {
public static function areaProvider(): array {
return [
[2, 2, 4],
[3, 2, 6],
[2, 3, 6],
];
}
#[Test]
#[DataProvider('areaProvider')]
public function areaWithProvider(int $height, int $width, float $expectedArea): void {
$r = new Rectangle($height, $width);
self::assertSame($expectedArea, $r->area);
}
}
Data generators
#[Small]
class RectangleTest extends TestCase {
public static function areaProvider(): \Generator {
yield [2, 2, 4];
yield [3, 2, 6];
yield [2, 3, 6];
}
#[Test]
#[DataProvider('areaProvider')]
public function areaWithProvider(int $height, int $width, float $expectedArea): void {
$r = new Rectangle($height, $width);
self::assertSame($expectedArea, $r->area);
}
}
Data generators
#[Small]
class RectangleTest extends TestCase {
public static function areaProvider(): \Generator {
yield '2x2=4' => [2, 2, 4];
yield '3x2=6' => [3, 2, 6];
yield '2x3=6' => [2, 3, 6];
}
#[Test]
#[DataProvider('areaProvider')]
public function areaWithProvider(int $height, int $width, float $expectedArea): void {
$r = new Rectangle($height, $width);
self::assertSame($expectedArea, $r->area);
}
}
Data generators error output
$ vendor/bin/phpunit --filter areaWithProvider
..F 6 / 6 (100%)
Time: 00:00.003, Memory: 14.00 MB
There was 1 failure:
1) RectangleTest::areaWithProvider@2x2=4 with data (2, 2, 5)
Failed asserting that 4.0 is identical to 5.0.
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.
Data generators error output
$ vendor/bin/phpunit --filter areaWithProvider@2x2=4
F 6 / 6 (100%)
Time: 00:00.003, Memory: 14.00 MB
There was 1 failure:
1) RectangleTest::areaWithProvider@2x2=4 with data (2, 2, 5)
Failed asserting that 4.0 is identical to 5.0.
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Named arguments
#[Small]
class RectangleTest extends TestCase {
public static function areaProvider(): \Generator {
yield '2x2=4' => [
'height' => 2,
'width' => 2,
'expectedArea' => 4,
];
yield '3x2=6' => [
'height' => 3,
'width' => 2,
'expectedArea' => 6,
];
yield '2x3=6' => [
'expectedArea' => 6, // Out of order is fine.
'height' => 2,
'width' => 3,
];
}
#[Test]
#[DataProvider('areaProvider')]
public function areaWithProvider(int $height, int $width, float $expectedArea): void {
$r = new Rectangle($height, $width);
self::assertSame($expectedArea, $r->area);
}
}
Multiple providers
#[Small]
class RectangleTest extends TestCase {
public static function squareProvider(): \Generator {
// ...
}
public static function rectProvider(): \Generator {
// ...
}
#[Test]
#[DataProvider('squareProvider')]
#[DataProvider('rectProvider')]
public function areaWithProvider(int $height, int $width, float $expectedArea): void {
$r = new Rectangle($height, $width);
self::assertSame($expectedArea, $r->area);
}
}
Complex providers
#[Large]
class RequestHandlerTest extends TestCase {
#[Test, DataProvider('handlersProvider')]
public function handlers(
string $method, string|UriInterface $uri, $headers = [], ?string $body = null,
int $expectedCode = 200, array $expectedHeaders = [], ?string $expectedBody = null,
): void {
$request = new ServerRequest($method, $uri, $headers, $body);
$handler = new RequestHandler();
$res = $handler->handle($request);
self::assertSame($expectedCode, $res->getStatusCode());
if ($expectedBody) {
self::assertSame($expectedBody, (string) $res->getBody());
}
foreach ($expectedHeaders as $header => $value) {
$v = $res->getHeaderLine($header);
self::assertNotNull($v);
self::assertSame($value, $v);
}
}
}
Complex providers
#[Large]
class RequestHandlerTest extends TestCase {
public static function handlersProvider(): \Generator {
yield 'just confirm a 200' => [
'method' => 'GET',
'uri' => '/beep',
'expectedBody' => 'boop', // Optional
];
yield 'confirm a not-found URL' => [
'method' => 'GET',
'uri' => '/missing',
'expectedCode' => 404, // Optional, and don't check the body
];
yield 'create an object' => [
'method' => 'POST',
'uri' => '/create',
'expectedCode' => 201, // Override default
'expectedHeaders' => ['Location' => '/beep/1'], // Triggers the foreach
];
}
}
Better complex providers
#[Large]
class RequestHandlerTest extends TestCase {
#[Test, DataProvider('handlersProvider')]
public function handlers(
string $method, string|UriInterface $uri, $headers = [], ?string $body = null,
int $expectedCode = 200, array $expectedHeaders = [], ?string $expectedBody = null,
): void {
$request = new ServerRequest($method, $uri, $headers, $body);
$handler = new RequestHandler();
$res = $handler->handle($request);
self::assertSame($expectedCode, $res->getStatusCode());
if ($expectedBody) {
self::assertSame($expectedBody, (string) $res->getBody());
}
foreach ($expectedHeaders as $header => $value) {
$v = $res->getHeaderLine($header);
self::assertNotNull($v);
self::assertSame($value, $v);
}
}
}
Objects FTW
#[Large]
class RequestHandlerTest extends TestCase {
#[Test, DataProvider('handlersProvider')]
public function handlers(
ServerRequest $request,
int $expectedCode = 200, array $expectedHeaders = [], ?string $expectedBody = null,
): void {
$handler = new RequestHandler();
$res = $handler->handle($request);
self::assertSame($expectedCode, $res->getStatusCode());
if ($expectedBody) {
self::assertSame($expectedBody, (string) $res->getBody());
}
foreach ($expectedHeaders as $header => $value) {
$v = $res->getHeaderLine($header);
self::assertNotNull($v);
self::assertSame($value, $v);
}
}
}
Simpler data provider, too
#[Large]
class RequestHandlerTest extends TestCase {
#[Test, DataProvider('handlersProvider')]
public function handlers(
ServerRequest $request,
int $expectedCode = 200, array $expectedHeaders = [], ?string $expectedBody = null,
): void {
$handler = new RequestHandler();
$res = $handler->handle($request);
// Assertions here
}
public static function handlersProvider(): \Generator {
yield 'just confirm a 200' => [
'request' => new ServerRequest('GET', '/beep'),
];
yield 'confirm a not-found URL' => [
'request' => new ServerRequest('GET', '/missing'),
'expectedCode' => 404,
];
yield 'create an object' => [
'request' => new ServerRequest('POST', '/create'),
'expectedCode' => 201,
'expectedHeaders' => ['Location' => '/beep/1'],
];
}
}
Can we do the same for assertions?
#[Large]
class RequestHandlerTest extends TestCase {
#[Test, DataProvider('handlersProvider')]
public function handlers(
ServerRequest $request,
int $expectedCode = 200, array $expectedHeaders = [], ?string $expectedBody = null,
): void {
$handler = new RequestHandler();
$res = $handler->handle($request);
self::assertSame($expectedCode, $res->getStatusCode());
if ($expectedBody) {
self::assertSame($expectedBody, (string) $res->getBody());
}
foreach ($expectedHeaders as $header => $value) {
$v = $res->getHeaderLine($header);
self::assertNotNull($v);
self::assertSame($value, $v);
}
}
public static function handlersProvider(): \Generator {
// ...
}
}
1. Self-validating complex providers
class RequestHandlerTest extends TestCase {
#[Test, DataProvider('handlersProvider')]
public function handlers(ServerRequest $request, \Closure $checks): void {
$handler = new RequestHandler();
$res = $handler->handle($request);
$checks($res);
}
public static function handlersProvider(): \Generator {
yield 'just confirm a 200' => [
'request' => new ServerRequest('GET', '/beep', []),
'checks' => static function(ResponseInterface $response) {
self::assertSame(200, $response->getStatusCode());
},
];
yield 'confirm a not-found URL' => [
'request' => new ServerRequest('GET', '/missing'),
'checks' => static function (ResponseInterface $response) {
self::assertSame(404, $response->getStatusCode());
},
];
yield 'create an object' => [
'request' => new ServerRequest('POST', '/create'),
'checks' => static function (ResponseInterface $response) {
self::assertSame(201, $response->getStatusCode());
self::assertSame('/beep/1', $response->getHeaderLine('Location'));
},
];
}
}
2. Custom macro-assertions
class RequestHandlerTest extends TestCase {
#[Test, DataProvider('handlersProvider')]
public function handlers(
ServerRequest $request,
int $expectedCode = 200, array $expectedHeaders = [], ?string $expectedBody = null,
): void {
$handler = new RequestHandler();
$res = $handler->handle($request);
self::assertSame($expectedCode, $res->getStatusCode());
self::assertBody($expectedBody, $res);
self::assertHeaders($expectedHeaders, $res);
}
protected static function assertBody(?string $expectedBody, ResponseInterface $res): void {
if ($expectedBody) {
self::assertSame($expectedBody, (string) $res->getBody());
}
}
protected static function assertHeaders(array $expectedHeaders, ResponseInterface $res): void {
foreach ($expectedHeaders as $header => $value) {
$v = $res->getHeaderLine($header);
self::assertNotNull($v);
self::assertSame($value, $v);
}
}
}
Summary
Simple cases: #[TestWith]
Most cases: Data providers
Always use a generator provider
Name your tests and your arguments
Assertion callbacks in tests are neat
Use custom assertion wrappers liberally
Test setup
The traditional way
class IntegrationTest extends TestCase {
private Connection $conn;
public function setUp(): void {
$connectionParams = new DsnParser()->parse('pdo-sqlite:///:memory:');
$this->conn = DriverManager::getConnection($connectionParams);
$this->conn->executeQuery('CREATE TABLE users ...');
}
#[Test]
public function stuff(): void {
$this->conn->insert('users', ['id' => 1, 'name' => 'Larry']);
// Do stuff ...
$result = $this->conn->executeQuery('SELECT ...')->fetchOne();
self::assertSame('Larry', $result);
}
}
The flexible way
class IntegrationTest extends TestCase {
private Connection $conn;
#[Before(20)]
public function setupDoctrine(): void {
$connectionParams = new DsnParser()->parse('pdo-sqlite:///:memory:');
$this->conn = DriverManager::getConnection($connectionParams);
}
#[Before(10)]
public function setupTables(): void {
$this->conn->executeQuery('CREATE TABLE users ...');
}
#[Test]
public function stuff(): void {
$this->conn->insert('users', ['id' => 1, 'name' => 'Larry']);
// Do stuff ...
$result = $this->conn->executeQuery('SELECT ...')->fetchOne();
self::assertSame('Larry', $result);
}
}
Also class-level options
class IntegrationTest extends TestCase {
private static Connection $conn;
#[BeforeClass(20)]
public static function setupDoctrine(): void {
$connectionParams = new DsnParser()->parse('pdo-sqlite:///:memory:');
self::$conn = DriverManager::getConnection($connectionParams);
}
#[BeforeClass(10)]
public static function setupTables(): void {
self::$conn->executeQuery('CREATE TABLE users ...');
}
#[Test]
public function stuff(): void {
self::$conn->insert('users', ['id' => 1, 'name' => 'Larry']);
// Do stuff ...
$result = $this->conn->executeQuery('SELECT ...')->fetchOne();
self::assertSame('Larry', $result);
}
}
#[After] and #[AfterClass]
class IntegrationTest extends TestCase {
private static Connection $conn;
#[BeforeClass(20)]
public static function setupDoctrine(): void {
$connectionParams = new DsnParser()->parse('pdo-sqlite:///:memory:');
self::$conn = DriverManager::getConnection($connectionParams);
}
#[BeforeClass(10)]
public static function setupTables(): void {
self::$conn->executeQuery('CREATE TABLE users ...');
}
#[After]
public function cleanupTables(): void {
self::$conn->executeQuery('DELETE FROM users');
}
#[Test]
public function stuff(): void {
self::$conn->insert('users', ['id' => 1, 'name' => 'Larry']);
// Do stuff ...
$result = $this->conn->executeQuery('SELECT ...')->fetchOne();
self::assertSame('Larry', $result);
}
}
But what if I want to reuse my setup methods? That means a utility base class, right?
Wrong!
Traits
<aside>
Traits are not evil, no matter what some fuddy duddy says
Traits that have no external dependences are fine
Using traits to bypass proper DI/API is evil
Yes, this means Laravel's trait usage is evil. (So is Laravel)
</aside>
Back to our code
class IntegrationTest extends TestCase {
private static Connection $conn;
#[BeforeClass(20)]
public static function setupDoctrine(): void {
$connectionParams = new DsnParser()->parse('pdo-sqlite:///:memory:');
self::$conn = DriverManager::getConnection($connectionParams);
}
#[BeforeClass(10)]
public static function setupTables(): void {
self::$conn->executeQuery('CREATE TABLE users ...');
}
#[After]
public function cleanupTables(): void {
self::$conn->executeQuery('DELETE FROM users');
}
#[Test]
public function stuff(): void {
self::$conn->insert('users', ['id' => 1, 'name' => 'Larry']);
// Do stuff ...
$result = $this->conn->executeQuery('SELECT ...')->fetchOne();
self::assertSame('Larry', $result);
}
}
Move utilities to traits
trait UseDoctrine {
private static Connection $conn;
#[BeforeClass(20)]
public static function setupDoctrine(): void {
$connectionParams = new DsnParser()->parse('pdo-sqlite:///:memory:');
self::$conn = DriverManager::getConnection($connectionParams);
}
}
trait WithUserTable {
use UseDoctrine;
#[BeforeClass(10)]
public static function setupTables(): void {
self::$conn->executeQuery('CREATE TABLE users ...');
}
#[After]
public function cleanupTables(): void {
self::$conn->executeQuery('DELETE FROM users');
}
}
class IntegrationTest extends TestCase {
use UseDoctrine;
use WithUserTable;
// Tests here
}
Assertion utilities
trait ResponseAssertions {
protected static function assertBody(?string $expectedBody, ResponseInterface $res): void {
// Same as before
}
protected static function assertHeaders(array $expectedHeaders, ResponseInterface $res): void {
// Same as before
}
}
class RequestHandlerTest extends TestCase {
use ResponseAssertions;
#[Test, DataProvider('handlersProvider')]
public function handlers(ServerRequest $request,
int $expectedCode = 200, array $expectedHeaders = [], ?string $expectedBody = null,
): void {
$handler = new RequestHandler();
$res = $handler->handle($request);
self::assertSame($expectedCode, $res->getStatusCode());
self::assertBody($expectedBody, $res);
self::assertHeaders($expectedHeaders, $res);
}
}
Transaction-based test isolation
trait TransactionIsolation {
use UseDoctrine;
#[Before]
public function startTransaction(): void {
self::$conn->beginTransaction();
}
#[After]
public function endTransaction(): void {
self::$conn->rollBack();
}
}
class IntegrationTest extends TestCase {
use UseDoctrine;
use TransactionIsolation;
// Tests here
}
I'm sure you can find other uses...
Documentation
Standard output
$ vendor/bin/phpunit tests/Shapes/RectangleTest.php
PHPUnit 13.0.5 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.11
Configuration: /home/crell/Projects/shapes/phpunit.xml
......... 9 / 9 (100%)
Time: 00:00.001, Memory: 14.00 MB
OK (9 tests, 9 assertions)
With --testdox
$ vendor/bin/phpunit tests/Shapes/RectangleTest.php --testdox
PHPUnit 13.0.5 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.11
Configuration: /home/crell/Projects/shapes/phpunit.xml
......... 9 / 9 (100%)
Time: 00:00.001, Memory: 14.00 MB
Rectangle (Crell\Shenanigans\Shapes\Rectangle)
✔ Area with provider with data set "2x2=4"
✔ Area with provider with data set "3x2=6"
✔ Area with provider with data set "2x3=6"
✔ Area with data set "2x2=4"
✔ Area with data set "3x2=6"
✔ Area with data set "2x3=6"
✔ Area of 2x 2is 4
✔ Area of 3x 2is 6
✔ Area of 2x 3is 6
OK (9 tests, 9 assertions)
public function translateFormElementValueTranslateRenderingOptionForConcreteFormAndConcreteSectionElementIfElementRenderingOptionsContainsATranslationFilesAndElementRenderingOptionIsNotEmptyAndRenderingOptionShouldBeTranslatedAndTranslationExists(): void
WAT?
Yes, this is real code from TYPO3
Separate code from English
#[Test]
#[TestDox('If translations are enabled and there are translation files, they should be used')]
public function translationsEnabledFiles(): void {
// ...
}
One-off test dox
#[TestDox('Rectangles can compute their area')]
class RectangleTest extends TestCase {
#[Test, TestDox('A 2x2 rectangle has an area of 4')]
public function areaOf2x2is4(): void {
// ...
}
#[Test, TestDox('A 3x2 rectangle has an area of 6')]
public function areaOf3x2is6(): void {
// ...
}
#[Test, TestDox('A 2x3 rectangle has an area of 6')]
public function areaOf2x3is6(): void {
// ...
}
}
$ vendor/bin/phpunit tests/Shapes/RectangleTest.php --testdox
// ...
Rectangles can compute their area
✔ A 2x2 rectangle has an area of 4
✔ A 3x2 rectangle has an area of 6
✔ A 2x3 rectangle has an area of 6
OK (3 tests, 3 assertions)
Data providers and test dox
#[TestDox('Rectangles can compute their area')]
class RectangleTest extends TestCase {
public static function areaProvider(): \Generator {
yield '2x2=4' => [ ... ];
yield '3x2=6' => [ ... ];
yield '2x3=6' => [ ... ];
}
#[Test, DataProvider('areaProvider')]
#[TestDox('A rectangle with height $height and width $width has an area of $expectedArea')]
public function areaWithProvider(int $height, int $width, float $expectedArea): void {
$r = new Rectangle($height, $width);
self::assertSame($expectedArea, $r->area);
}
}
$ vendor/bin/phpunit tests/Shapes/RectangleTest.php --testdox
// ...
Rectangles can compute their area
✔ A rectangle with height 2 and width 2 has an area of 4
✔ A rectangle with height 3 and width 2 has an area of 6
✔ A rectangle with height 2 and width 3 has an area of 6
OK (3 tests, 3 assertions)
$_dataName to get the dataset name
Custom dox formatters
#[Large, TestDox('RequestHandler returns expected response')]
class RequestHandlerTest extends TestCase {
#[Test, DataProvider('handlersProvider')]
#[TestDoxFormatter('handlersFormatter')]
public function handlers(
ServerRequest $request,
int $expectedCode = 200, array $expectedHeaders = [], ?string $expectedBody = null,
): void {
// ...
}
public static function handlersFormatter(
ServerRequest $request,
int $expectedCode = 200, array $expectedHeaders = [], ?string $expectedBody = null,
): string {
$formattedRequest = sprintf('%s %s', $request->getMethod(), $request->getUri()->getPath());
return sprintf('%s responds with %d', $formattedRequest, $expectedCode);
}
public static function handlersProvider(): \Generator { ... }
}
$ vendor/bin/phpunit tests/Shapes/RectangleTest.php --testdox
// ...
RequestHandler returns expected response
✔ GET /missing responds with 404
✔ POST /create responds with 201
✔ GET /beep responds with 200
OK (3 tests, 5 assertions)
Test organization
Test isolation is good
Sometimes you just need to hit the DB
Those tests will be slow
Based on a true story
class Sync {
public function __construct(
private Client $client,
private Connection $conn,
) {}
public function getData(): string {
$this->client->send('...');
return $this->client->getResponse()->getBody()->toString();
}
public function saveData(string $data): void {
$this->conn->insert('synced_data', ['data' => $data]);
}
public function cleanData(string $data): string {
return strtolower($data);
}
}
These tests are rude
class SyncTest extends TestCase {
use UseDoctrine;
private Sync $sync;
#[Before]
public function makeSubject(): void {
$this->sync = new Sync(new Client(), $this->conn);
}
#[Test]
public function cleaning(): void {
self::assertSame('blah', $this->sync->cleanData('Blah'));
}
#[Test]
public function writeToDb(): void {
$this->sync->saveData('...');
self::assertEquals('...', $this->conn->executeQuery("...")->fetchOne());
}
#[Test]
public function getData(): void {
$data = $this->sync->getData();
// ...
}
}
These tests are polite
#[Group('Sync')]
class SyncTest extends TestCase {
use UseDoctrine;
private Sync $sync;
#[Before]
public function makeSubject(): void {
$this->sync = new Sync(new Client(), $this->conn);
}
#[Test]
public function cleaning(): void {
self::assertSame('blah', $this->sync->cleanData('Blah'));
}
#[Test, Group('database')]
public function writeToDb(): void {
$this->sync->saveData('...');
self::assertEquals('...', $this->conn->executeQuery("...")->fetchOne());
}
#[Test, Group('connected')]
public function getData(): void {
$data = $this->sync->getData();
// ...
}
}
Run just some groups
$ vendor/bin/phpunit tests/Groups/SyncTest.php
PHPUnit 13.0.5 by Sebastian Bergmann and contributors.
// ...
... 3 / 3 (100%)
Time: 00:00.001, Memory: 14.00 MB
OK (3 tests, 3 assertions)
$ vendor/bin/phpunit tests/Groups/SyncTest.php --group connected
PHPUnit 13.0.5 by Sebastian Bergmann and contributors.
// ...
. 1 / 1 (100%)
Time: 00:00.001, Memory: 14.00 MB
OK (1 tests, 1 assertions)
$ vendor/bin/phpunit tests/Groups/SyncTest.php --exclude-group connected
PHPUnit 13.0.5 by Sebastian Bergmann and contributors.
// ...
.. 2 / 2 (100%)
Time: 00:00.001, Memory: 14.00 MB
OK (2 tests, 2 assertions)
Fully exclude in phpunit.xml
connecteddatabase
Override with phpunit --group connected
#[Small], #[Medium], #[Large] are automatically groups as well
Apply liberally
Driver tests
New sample code
class Runner {
public function __construct(
readonly private Backend $backend,
) {}
public function run($stuff): string {
return $this->backend->doStuff($stuff);
}
}
interface Backend {
public function doStuff(string $stuff): string;
}
class SqlBackend implements Backend {
public function doStuff(string $stuff): string { /* ... */ }
}
class RedisBackend implements Backend {
public function doStuff(string $stuff): string { /* ... */ }
}
Test base class
abstract class RunnerTestCases extends TestCase {
protected Backend $backend;
abstract function setupBackend(): void;
public static function runProvider(): \Generator {
yield ['a'];
yield ['b'];
}
#[DataProvider('runProvider')]
public function testRun(string $val): void {
$runner = new Runner($this->backend);
self::assertSame($val, $runner->run($val));
}
}
Driver-specific tests
#[Group('sql'), Group('runner')]
class SqlRunnerTest extends RunnerTestCases {
#[Before]
public function setupBackend(): void {
$this->backend = new SqlBackend();
}
}
#[Group('redis'), Group('runner')]
class RedisRunnerTest extends RunnerTestCases {
#[Before]
public function setupBackend(): void {
$this->backend = new RedisBackend();
}
}
Need a new driver?
#[Group('memory'), Group('runner')]
class MemoryRunnerTest extends RunnerTestCases {
#[Before]
public function setupBackend(): void {
$this->backend = new MemoryBackend();
}
}
class MemoryBackend implements Backend {
public function doStuff(string $stuff): string {
return $stuff;
}
}
#[Group('json'), Medium]
class JsonFormatterTest extends ArrayBasedFormatterTestCases {
#[Before]
public function setupFormatter(): void {
$this->formatters = [new JsonFormatter()];
$this->format = 'json';
// ...
}
protected function arrayify(mixed $serialized): array {
return json_decode($serialized, true, 512, JSON_THROW_ON_ERROR);
}
}
Let's review
#[TestWith], #[DataProvider] make writing many tests easier
Generators, Test callbacks, Custom assertions
Name everything
Use #[Before] etc. always, never setUp()
Traits are good, actually! (In the right situation)
#[TestDox] are your friend
#[Group]s are your friend
PHPUnit is your friend!
* Who you are
* Use PHPUnit
* Believe in testing
* Want to do it better
* This talk is not
* An intro to PHPUnit basics
* An intro to the value of testing
* Focused on PHPUnit 12/13
* Topics
* Custom cache dir
* S/M/L tests
* Data providers
* #[TestWith]
* provider methods
* use generators for provider methods, always
* Name your test cases
* Named arguments for provider methods
* Pass extra validation as a test case argument
* #[Before]\#[After] instead of setUp()
* Traits for tests, better than subclasses
* Package up Before/After pairs
* Transaction start/rollback trait
* Ensure schema is up to date trait
* TestDox
* Use #[Test] attribute, not test prefix.
* #[TestDox]
* #[TestDoxFormatter]
* Base abstract test class with impl-specific subclasses. (cf, Serde)
* Groups
* Use for excluding tests that require network, but you still want sometimes in dev or CI
* Can have many groups, so be liberal with them
*
* Mocks
* Avoid them in favor of anon classes
* If must use them, mocks vs stubs in PHPUnit (cf SB's article)