Skip to content

Commit 4b8c454

Browse files
committed
Initial commit, support for PDO postgres
0 parents  commit 4b8c454

34 files changed

Lines changed: 1197 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: 'Tests'
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
tests:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v2
16+
- name: Test
17+
uses: isbang/compose-action@1.4.1
18+
with:
19+
compose-file: './docker-compose.yml'
20+
down-flags: '--volumes'
21+
services: |
22+
tests

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.
2+
..
3+
/vendor
4+
.phpunit.cache
5+
.phpunit.result.cache

README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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

composer.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "cspray/database-test-case",
3+
"description": "A PHPUnit TestCase for asserting expectations on a database",
4+
"type": "library",
5+
"keywords": [
6+
"testing",
7+
"database"
8+
],
9+
"license": ["MIT"],
10+
"require": {
11+
"php": "^8.2",
12+
"phpunit/phpunit": "^10.0"
13+
},
14+
"require-dev": {
15+
"ext-pdo": "*",
16+
"ext-pdo_pgsql": "*",
17+
"roave/security-advisories": "dev-latest"
18+
},
19+
"autoload": {
20+
"psr-4": {
21+
"Cspray\\DatabaseTestCase\\": "src"
22+
}
23+
},
24+
"autoload-dev": {
25+
"psr-4": {
26+
"Cspray\\DatabaseTestCase\\Tests\\": "tests"
27+
}
28+
},
29+
"suggest": {
30+
"ext-pdo": "To enable the PdoConnectionAdapter"
31+
}
32+
}

docker-compose.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
version: '3.8'
2+
3+
services:
4+
postgres:
5+
build:
6+
context: .
7+
dockerfile: docker/postgres/Dockerfile
8+
volumes:
9+
- pgdata:/var/lib/postgresql/data
10+
restart: unless-stopped
11+
environment:
12+
- POSTGRES_PASSWORD=postgres
13+
networks:
14+
databasetestcase:
15+
16+
tests:
17+
build:
18+
context: .
19+
dockerfile: docker/php/Dockerfile
20+
target: app
21+
depends_on:
22+
postgres:
23+
condition: service_healthy
24+
volumes:
25+
- ./src:/app/src
26+
- ./tests:/app/tests
27+
- ./resources:/app/resources
28+
- ./phpunit.xml:/app/phpunit.xml
29+
- ./composer.json:/app/composer.json
30+
networks:
31+
databasetestcase:
32+
33+
networks:
34+
databasetestcase:
35+
36+
volumes:
37+
pgdata:

docker/php/Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM php:8.2.1-zts-bullseye AS php
2+
3+
RUN apt-get update -y \
4+
&& apt-get upgrade -y
5+
6+
RUN apt-get install git libsodium-dev libzip-dev libpq-dev -y
7+
RUN docker-php-ext-install sodium zip pdo pdo_pgsql
8+
9+
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
10+
11+
COPY --from=composer:2.1 /usr/bin/composer /usr/bin/composer
12+
13+
FROM php AS app
14+
15+
RUN mkdir /app
16+
COPY src /app/src
17+
COPY tests /app/tests
18+
COPY composer.json phpunit.xml /app/
19+
20+
WORKDIR /app
21+
22+
RUN COMPOSER_ALLOW_SUPERUSER=1 composer validate \
23+
&& composer install --no-scripts
24+
25+
CMD ["/app/vendor/bin/phpunit"]

docker/postgres/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM postgres:15-bullseye
2+
3+
COPY /resources/schemas/postgres.sql /docker-entrypoint-initdb.d/
4+
5+
HEALTHCHECK --interval=5s --start-period=7s --retries=5 --timeout=5s CMD pg_isready -d postgres
6+
7+
USER postgres

phpunit.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
4+
bootstrap="vendor/autoload.php"
5+
cacheDirectory=".phpunit.cache"
6+
executionOrder="depends,defects"
7+
requireCoverageMetadata="true"
8+
beStrictAboutCoverageMetadata="true"
9+
beStrictAboutOutputDuringTests="true"
10+
failOnRisky="true"
11+
failOnWarning="true">
12+
<testsuites>
13+
<testsuite name="unit">
14+
<directory>tests/Unit</directory>
15+
</testsuite>
16+
<testsuite name="integration">
17+
<directory>tests/Integration</directory>
18+
</testsuite>
19+
</testsuites>
20+
21+
<coverage>
22+
<include>
23+
<directory suffix=".php">src</directory>
24+
</include>
25+
</coverage>
26+
</phpunit>

resources/schemas/postgres.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE TABLE my_table (
2+
id SERIAL PRIMARY KEY,
3+
name VARCHAR(255),
4+
created_at TIMESTAMP DEFAULT now()
5+
);

src/ConnectionAdapter.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Cspray\DatabaseTestCase;
4+
5+
use Cspray\DatabaseTestCase\DatabaseRepresentation\Table;
6+
7+
interface ConnectionAdapter {
8+
9+
public function establishConnection() : void;
10+
11+
public function onTestStart() : void;
12+
13+
public function onTestStop() : void;
14+
15+
public function closeConnection() : void;
16+
17+
public function loadFixture(Fixture $fixture, Fixture... $additionalFixture) : void;
18+
19+
public function getUnderlyingConnection() : object;
20+
21+
public function getTable(string $name) : Table;
22+
23+
}

0 commit comments

Comments
 (0)