Skip to content

Commit 6f4258c

Browse files
authored
Introduce trait SharedDatabases (#92)
2 parents 6a73754 + 7700dfa commit 6f4258c

File tree

3 files changed

+289
-0
lines changed

3 files changed

+289
-0
lines changed

.github/workflows/php.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,36 @@ jobs:
5454
php: ['8.2', '8.3', '8.4']
5555
os: ['ubuntu-latest']
5656

57+
services:
58+
mysql:
59+
image: mariadb
60+
env:
61+
MYSQL_ROOT_PASSWORD: root
62+
MYSQL_DATABASE: icinga_unittest
63+
MYSQL_USER: icinga_unittest
64+
MYSQL_PASSWORD: icinga_unittest
65+
options: >-
66+
--health-cmd "mariadb -s -uroot -proot -e'SHOW DATABASES;' 2> /dev/null | grep icinga_unittest > test"
67+
--health-interval 10s
68+
--health-timeout 5s
69+
--health-retries 5
70+
ports:
71+
- 3306/tcp
72+
73+
pgsql:
74+
image: postgres
75+
env:
76+
POSTGRES_USER: icinga_unittest
77+
POSTGRES_PASSWORD: icinga_unittest
78+
POSTGRES_DB: icinga_unittest
79+
options: >-
80+
--health-cmd pg_isready
81+
--health-interval 10s
82+
--health-timeout 5s
83+
--health-retries 5
84+
ports:
85+
- 5432/tcp
86+
5787
steps:
5888
- name: Checkout code base
5989
uses: actions/checkout@v5
@@ -69,4 +99,15 @@ jobs:
6999
run: composer install -n --no-progress
70100

71101
- name: PHPUnit
102+
env:
103+
MYSQL_TESTDB: icinga_unittest
104+
MYSQL_TESTDB_HOST: 127.0.0.1
105+
MYSQL_TESTDB_PORT: ${{ job.services.mysql.ports['3306'] }}
106+
MYSQL_TESTDB_USER: icinga_unittest
107+
MYSQL_TESTDB_PASSWORD: icinga_unittest
108+
PGSQL_TESTDB: icinga_unittest
109+
PGSQL_TESTDB_HOST: 127.0.0.1
110+
PGSQL_TESTDB_PORT: ${{ job.services.pgsql.ports['5432'] }}
111+
PGSQL_TESTDB_USER: icinga_unittest
112+
PGSQL_TESTDB_PASSWORD: icinga_unittest
72113
run: phpunit --verbose

src/Test/SharedDatabases.php

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
3+
namespace ipl\Sql\Test;
4+
5+
use ipl\Sql\Config;
6+
use ipl\Sql\Connection;
7+
use RuntimeException;
8+
9+
/**
10+
* Data provider for database connections. Use this to provide real database connections for your tests.
11+
*
12+
* To use it, implement {@see Databases::setUpSchema()} and {@see Databases::tearDownSchema()}.
13+
* The environment also needs to provide the following variables: (Replace * with the name of a supported adapter)
14+
* {@see Databases::SUPPORTED_ADAPTERS}
15+
*
16+
* Name | Description
17+
* ----------------- | ------------------------
18+
* *_TESTDB | The database to use
19+
* *_TESTDB_HOST | The server to connect to
20+
* *_TESTDB_PORT | The port to connect to
21+
* *_TESTDB_USER | The user to connect with
22+
* *_TESTDB_PASSWORD | The password of the user
23+
*
24+
* Each test case will run multiple times, once for each database.
25+
* The connection is passed as the first argument to it.
26+
*
27+
* A schema will be initialized once the first test case using this provider is run. Schemas will first be dropped
28+
* and then recreated to ensure a clean state. During the entire test run, the same schema will be used for all
29+
* tests.
30+
*
31+
* If you need to implement your own setUp() and tearDown() methods, and need access to the database connection,
32+
* use {@see Databases::getConnection()}.
33+
*/
34+
trait SharedDatabases
35+
{
36+
/**
37+
* All database connections
38+
*
39+
* @internal Only the trait itself should access this property
40+
*
41+
* @var array
42+
*/
43+
private static array $connections = [];
44+
45+
/** @var string[] */
46+
private const SUPPORTED_ADAPTERS = ['mssql', 'mysql', 'oracle', 'pgsql', 'sqlite'];
47+
48+
/**
49+
* Create the schema for the test database
50+
*
51+
* @param Connection $db
52+
* @param string $driver
53+
*
54+
* @return void
55+
*/
56+
abstract protected static function setUpSchema(Connection $db, string $driver): void;
57+
58+
/**
59+
* Drop the schema of the test database
60+
*
61+
* @param Connection $db
62+
* @param string $driver
63+
*
64+
* @return void
65+
*/
66+
abstract protected static function tearDownSchema(Connection $db, string $driver): void;
67+
68+
/**
69+
* Provide the database connections
70+
*
71+
* @return array<string, Connection[]>
72+
*/
73+
final public static function sharedDatabases(): array
74+
{
75+
self::initializeDatabases();
76+
77+
return self::$connections;
78+
}
79+
80+
/**
81+
* Get the current database connection
82+
*
83+
* @return Connection
84+
* @throws RuntimeException if the connection cannot be retrieved
85+
*/
86+
final protected function getConnection(): Connection
87+
{
88+
if (method_exists($this, 'getProvidedData')) {
89+
$connections = $this->getProvidedData();
90+
} elseif (method_exists($this, 'providedData')) {
91+
$connections = $this->providedData();
92+
} else {
93+
throw new RuntimeException('Cannot get connection: Unsupported PHPUnit version?');
94+
}
95+
96+
$connection = $connections[0];
97+
if (! $connection instanceof Connection) {
98+
throw new RuntimeException('Cannot get connection: Are all test cases using the same provider?');
99+
}
100+
101+
return $connection;
102+
}
103+
104+
/**
105+
* Get the value of an environment variable
106+
*
107+
* @param string $name
108+
*
109+
* @return string
110+
*
111+
* @throws RuntimeException if the environment variable is not set
112+
*/
113+
final protected static function getEnvironmentVariable(string $name): string
114+
{
115+
$value = getenv($name);
116+
if ($value === false) {
117+
throw new RuntimeException("Environment variable $name is not set");
118+
}
119+
120+
return $value;
121+
}
122+
123+
/**
124+
* Get the connection configuration for the test database
125+
*
126+
* @param string $driver
127+
*
128+
* @return Config
129+
*/
130+
final protected static function getConnectionConfig(string $driver): Config
131+
{
132+
return new Config([
133+
'db' => $driver,
134+
'host' => self::getEnvironmentVariable(strtoupper($driver) . '_TESTDB_HOST'),
135+
'port' => self::getEnvironmentVariable(strtoupper($driver) . '_TESTDB_PORT'),
136+
'username' => self::getEnvironmentVariable(strtoupper($driver) . '_TESTDB_USER'),
137+
'password' => self::getEnvironmentVariable(strtoupper($driver) . '_TESTDB_PASSWORD'),
138+
'dbname' => self::getEnvironmentVariable(strtoupper($driver) . '_TESTDB')
139+
]);
140+
}
141+
142+
/**
143+
* Create a database connection
144+
*
145+
* @param string $driver
146+
*
147+
* @return Connection
148+
*
149+
* @internal Only the trait itself should call this method
150+
*/
151+
final protected static function connectToDatabase(string $driver): Connection
152+
{
153+
return new Connection(self::getConnectionConfig($driver));
154+
}
155+
156+
/**
157+
* Set up the database connections
158+
*
159+
* @return void
160+
*
161+
* @internal Only the trait itself should call this method
162+
*/
163+
final protected static function initializeDatabases(): void
164+
{
165+
foreach (self::SUPPORTED_ADAPTERS as $driver) {
166+
if (isset($_SERVER[strtoupper($driver) . '_TESTDB'])) {
167+
if (! isset(self::$connections[$driver])) {
168+
self::$connections[$driver] = [self::connectToDatabase($driver)];
169+
static::tearDownSchema(self::$connections[$driver][0], $driver);
170+
static::setUpSchema(self::$connections[$driver][0], $driver);
171+
}
172+
}
173+
}
174+
}
175+
}

tests/SharedDatabasesTest.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace ipl\Tests\Sql;
4+
5+
use ipl\Sql\Connection;
6+
use ipl\Sql\Select;
7+
use ipl\Sql\Test\SharedDatabases;
8+
9+
/**
10+
* A test for a test component! Yay!
11+
*/
12+
class SharedDatabasesTest extends TestCase
13+
{
14+
use SharedDatabases;
15+
16+
/** @dataProvider sharedDatabases */
17+
public function testInsert(Connection $db)
18+
{
19+
// This is the first case, so the table must have been dropped and be empty
20+
$result = $db->select((new Select())->columns('name')->from('test'))->fetchAll();
21+
$this->assertEmpty($result);
22+
23+
$db->insert('test', ['name' => 'test']);
24+
$db->insert('test', ['name' => 'test2']);
25+
}
26+
27+
/**
28+
* @depends testInsert
29+
* @dataProvider sharedDatabases
30+
*/
31+
public function testSelect(Connection $db)
32+
{
33+
// The previous case inserts "name=test" but tearDown removes it
34+
$result = $db->select((new Select())->columns('name')->from('test'))->fetchAll();
35+
$this->assertCount(1, $result);
36+
$this->assertSame('test2', $result[0]['name']);
37+
}
38+
39+
/**
40+
* @depends testSelect
41+
* @dataProvider sharedDatabases
42+
*/
43+
public function testUpdate(Connection $db)
44+
{
45+
$stmt = $db->update('test', ['name' => 'test3'], ['name = ?' => 'test2']);
46+
$this->assertEquals(1, $stmt->rowCount());
47+
}
48+
49+
/**
50+
* @depends testUpdate
51+
* @dataProvider sharedDatabases
52+
*/
53+
public function testDelete(Connection $db)
54+
{
55+
$stmt = $db->delete('test', ['name = ?' => 'test3']);
56+
$this->assertEquals(1, $stmt->rowCount());
57+
}
58+
59+
protected static function setUpSchema(Connection $db, string $driver): void
60+
{
61+
$db->exec('CREATE TABLE test (name VARCHAR(255))');
62+
}
63+
64+
protected static function tearDownSchema(Connection $db, string $driver): void
65+
{
66+
$db->exec('DROP TABLE IF EXISTS test');
67+
}
68+
69+
public function tearDown(): void
70+
{
71+
$this->getConnection()->delete('test', ['name = ?' => ['test']]);
72+
}
73+
}

0 commit comments

Comments
 (0)