If this is not you, one of us is in the wrong room
<phpunit
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
<phpunit
bootstrap="vendor/autoload.php"
cacheDirectory="build/phpunit.cache"
>
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
<phpunit
bootstrap="vendor/autoload.php"
cacheDirectory="build/phpunit.cache"
failOnNotice="true"
failOnWarning="true"
>
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
<phpunit
bootstrap="vendor/autoload.php"
cacheDirectory="build/phpunit.cache"
failOnNotice="true"
failOnWarning="true"
>
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreIndirectDeprecations="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
<phpunit
bootstrap="vendor/autoload.php"
cacheDirectory="build/phpunit.cache"
failOnNotice="true"
failOnWarning="true"
>
<!-- ... -->
<php>
<ini name="display_errors" value="on" />
<ini name="display_startup_errors" value="on" />
<ini name="error_reporting" value="-1" />
<ini name="zend.assertions" value="1" />
<ini name="assert.exceptions" value="1" />
<ini name="xdebug.show_show_exception_trace" value="on" />
</php>
</phpunit>
<phpunit
bootstrap="vendor/autoload.php"
cacheDirectory="build/phpunit.cache"
failOnNotice="true"
failOnWarning="true"
>
<!-- ... -->
<php>
<!-- ... -->
<env name="APP_MODE" value="dev" />
</php>
</phpunit>
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);
}
}
#[Small]
class RectangleTest extends TestCase {
#[Test]
public function areaOf2x2is4(): void {
$r = new Rectangle(2, 2);
self::assertSame(4.0, $r->area);
}
}
#[Medium] and #[Large]
#[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);
}
}
#[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);
}
}
#[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);
}
}
#[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);
}
}
$ 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.
$ 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.
#[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);
}
}
#[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);
}
}
#[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);
}
}
}
#[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
];
}
}
#[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);
}
}
}
#[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);
}
}
}
#[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'],
];
}
}
#[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 {
// ...
}
}
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'));
},
];
}
}
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);
}
}
}
#[TestWith]
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);
}
}
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);
}
}
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
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);
}
}
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
}
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);
}
}
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...
$ 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)
--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
#[Test]
#[TestDox('If translations are enabled and there are translation files, they should be used')]
public function translationsEnabledFiles(): void {
// ...
}
#[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)
#[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
#[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)
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);
}
}
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();
// ...
}
}
#[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();
// ...
}
}
$ 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)
phpunit.xml
connected
database
Override with phpunit --group connected
#[Small], #[Medium], #[Large] are automatically groups as well
Apply liberally
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 { /* ... */ }
}
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));
}
}
#[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();
}
}
#[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;
}
}
(A bit simplified)
class SerdeCommon extends Serde {
/** @var Formatter[] */
protected readonly array $formatters;
// ...
public function __construct(
// ...
array $formatters = [],
) {
// ...
$this->formatters = array_filter($formatters typeIs(Formatter::class));
$this->deformatters = array_filter($formatters typeIs(Deformatter::class));
}
}
abstract class SerdeTestCases extends TestCase {
protected array $formatters;
protected string $format;
abstract protected function arrayify(mixed $serialized): array;
protected function validateSerialized(mixed $serialized, string $name): void {
$validateMethod = str_replace([' ', ':', ','], '_', $name) . '_validate';
if (method_exists($this, $validateMethod)) {
$this->$validateMethod($serialized);
}
}
#[Test, DataProvider('round_trip_examples')]
#[DataProvider('value_object_flatten_examples')]
// Lots more data providrs here...
public function round_trip(object $data): void {
$s = new SerdeCommon(formatters: $this->formatters);
$serialized = $s->serialize($data, $this->format);
// dataName() is marked internal, but seems stable enough.
$this->validateSerialized($serialized, (string)$this->dataName());
$result = $s->deserialize($serialized, from: $this->format, to: $data::class);
self::assertEquals($data, $result);
}
}
abstract class ArrayBasedFormatterTestCases extends SerdeTestCases {
protected function point_validate(mixed $serialized): void {
$toTest = $this->arrayify($serialized);
self::assertEquals(1, $toTest['x']);
self::assertEquals(2, $toTest['y']);
self::assertEquals(3, $toTest['z']);
}
// ...
}
#[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);
}
}
#[TestWith], #[DataProvider] make writing many tests easier
#[Before] etc. always, never setUp()#[TestDox] are your friend#[Group]s are your friendPHPUnit is your friend!