|
| 1 | +# DatabaseTestCase |
| 2 | + |
| 3 | +A library to facilitate testing database interactions using PHPUnit 10+. |
| 4 | + |
| 5 | +Features this library currently provides: |
| 6 | + |
| 7 | +- Handles typical database setup and teardown |
| 8 | +- Simple representation of a table's rows |
| 9 | +- Mechanism for loading fixture data specific to each test |
| 10 | + |
| 11 | +Features this library **does not** currently provide, but plans to: |
| 12 | + |
| 13 | +- Semantic assertions on the state of a database |
| 14 | +- Representation for the information schema of a given table |
| 15 | + |
| 16 | +The rest of this document details how to install this library, make use of its `TestCase`, and what database |
| 17 | +connection objects are supported out-of-the-box. |
| 18 | + |
| 19 | +## Installation |
| 20 | + |
| 21 | +[Composer](https://getcomposer.org/) is the only supported method for installing this library. |
| 22 | + |
| 23 | +``` |
| 24 | +composer require --dev cspray/database-test-case |
| 25 | +``` |
| 26 | + |
| 27 | +## Usage Guide |
| 28 | + |
| 29 | +Using this library starts by creating a PHPUnit test that extends `Cspray\DatabaseTestCase\DatabaseTestCase`. This class |
| 30 | +overrides various setup and teardown functions provided by PHPUnit to ensure that a database connection is established |
| 31 | +and that database interactions happen against a known state. The `DatabaseTestCase` requires implementations |
| 32 | +to provide a `Cspray\DatabaseTestCase\ConnectionAdapter`. This implementation is ultimately responsible for calls to the |
| 33 | +database required by the testing framework. The `ConnectionAdapter` also provides access to the underlying connection, |
| 34 | +for example a `PDO` instance, that you can use in your code under test. Check out the section titled "Database Connections" |
| 35 | +for `ConnectionAdapter` instances supported out-of-the-box and how you could implement your own. |
| 36 | + |
| 37 | +In our example, going to assume that you have a PostgreSQL database with a table that has |
| 38 | +the following DDL: |
| 39 | + |
| 40 | +```postgresql |
| 41 | +CREATE TABLE my_table ( |
| 42 | + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), |
| 43 | + username VARCHAR(255), |
| 44 | + email VARCHAR(255), |
| 45 | + is_active BOOLEAN |
| 46 | +) |
| 47 | +``` |
| 48 | + |
| 49 | +Now, we can write a series of tests that interact with the database. |
| 50 | + |
| 51 | +```php |
| 52 | +<?php declare(strict_types=1); |
| 53 | + |
| 54 | +namespace Cspray\DatabaseTestCase\Demo; |
| 55 | + |
| 56 | +use Cspray\DatabaseTestCase\DatabaseRepresentation\Row;use Cspray\DatabaseTestCase\DatabaseTestCase; |
| 57 | +use Cspray\DatabaseTestCase\LoadFixture;use Cspray\DatabaseTestCase\SingleRecordFixture;use PDO; |
| 58 | + |
| 59 | +class MyDemoTest extends DatabaseTestCase { |
| 60 | + |
| 61 | + // Generally speaking you shouldn't call this method yourself! |
| 62 | + protected static function getConnectionAdapter() : ConnectionAdapter { |
| 63 | + // Be sure to change these configuration values to match your test setup! |
| 64 | + return new PdoConnectionAdapter( |
| 65 | + new ConnectionAdapterConfig( |
| 66 | + database: 'postgres', |
| 67 | + host: 'localhost', |
| 68 | + port: 5432, |
| 69 | + user: 'postgres', |
| 70 | + password: 'postgres' |
| 71 | + ), |
| 72 | + PdoDriver::Postgresql |
| 73 | + ); |
| 74 | + } |
| 75 | + |
| 76 | + public function testUnderlyingConnection() : void { |
| 77 | + // You'd pass the value of this method into your code under test |
| 78 | + // Use a different ConnectionAdapter if you aren't working with PDO! |
| 79 | + self::assertInstanceOf(PDO::class, self::getUnderlyingConnection()); |
| 80 | + } |
| 81 | + |
| 82 | + public function testShowEmptyTable() : void { |
| 83 | + // DatabaseTestCase provides a method to get a representation of a database table |
| 84 | + $table = $this->getTable('my_table'); |
| 85 | + |
| 86 | + // The $table is Countable, the count represents the number of rows in the table |
| 87 | + self::assertCount(0, $table); |
| 88 | + |
| 89 | + // The $table is iterable, each iteration yields a Row, but our database is empty! |
| 90 | + self::assertSame([], iterator_to_array($table)); |
| 91 | + } |
| 92 | + |
| 93 | + // Pass any number of Fixture to have corresponding FixtureRecords inserted into |
| 94 | + // the database before your test starts |
| 95 | + #[LoadFixture( |
| 96 | + new SingleRecordFixture('my_table', ['username' => 'cspray', 'email' => 'cspray@example.com', 'is_active' => true]), |
| 97 | + new SingleRecordFixture('my_table', ['username' => 'dyana', 'email' => 'dyana@example.com', 'is_active' => true]) |
| 98 | + )] |
| 99 | + public function testLoadingFixtures() : void { |
| 100 | + $table = $this->getTable('my_table'); |
| 101 | + |
| 102 | + self::assertCount(2, $table); |
| 103 | + self::assertContainsOnlyInstancesOf(Row::class, iterator_to_array($table)); |
| 104 | + self::assertSame('cspray', $table->getRow(0)->get('username')); |
| 105 | + self::assertSame('dyana@example.com', $table->getRow(1)->get('email')); |
| 106 | + self::assertNull($table->getRow(2)); |
| 107 | + } |
| 108 | + |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +### TestCase Hooks |
| 113 | + |
| 114 | +There are several critical things the `DatabaseTestCase` must take care of for database tests to work properly. To do that |
| 115 | +we must do something in all the normally used PHPUnit `TestCase` hooks. To be clear those methods are: |
| 116 | + |
| 117 | +- `TestCase::setUpBeforeClass` |
| 118 | +- `TestCase::setUp` |
| 119 | +- `TestCase::tearDown` |
| 120 | +- `TestCase::tearDownAfterClass` |
| 121 | + |
| 122 | +To make sure that `DatabaseTestCase` processes these hooks correctly they have been marked as `final`. There are new |
| 123 | +methods that have been provided that allow for the same effective hooks. |
| 124 | + |
| 125 | +| Old Hook | New Hook | |
| 126 | +| --- | --- | |
| 127 | +| `TestCase::setUpBeforeClass` | `DatabaseTestCase::beforeAll` | |
| 128 | +| `TestCase::setUp` | `DatabaseTestCase::beforeEach` | |
| 129 | +| `TestCase::tearDown` | | `DatabaseTestCase::afterEach` | |
| 130 | +| `TestCase::tearDownAfterClass` | `DatabaseTestCase::afterAll` | |
| 131 | + |
| 132 | +## Database Connections |
| 133 | + |
| 134 | +| Connection Instance | Library | Database | Implemented | |
| 135 | +|-----------------------------|-----------------------------------|-----------|--------------------| |
| 136 | +| `PDO` | [PHP PDO][pdo] | PostgreSQL | :white_check_mark: | |
| 137 | +| `PDO` | [PHP PDO][pdo] | MySQL | :x: | |
| 138 | +| `Amp\Postgres\PostgresLink` | [amphp/postgres@^2][amp-postgres] | PostgreSQL | :x: | |
| 139 | +| `Amp\Mysql\MysqlLink` | [amphp/mysql@^3][amp-mysql] | MySQL | :x: | |
| 140 | + |
| 141 | +[amp-mysql]: https://github.com/amphp/mysql |
| 142 | +[amp-postgres]: https://github.com/amphp/postgres |
| 143 | +[pdo]: https://php.net/pdo |
0 commit comments