Skip to content

Commit cd39b60

Browse files
authored
feat(database): add testing utilities (#1585)
1 parent a572b26 commit cd39b60

File tree

4 files changed

+279
-14
lines changed

4 files changed

+279
-14
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
namespace Tempest\Database\Testing;
4+
5+
use InvalidArgumentException;
6+
use PHPUnit\Framework\Assert;
7+
use PHPUnit\Framework\ExpectationFailedException;
8+
use Tempest\Container\Container;
9+
use Tempest\Database\Migrations\MigrationManager;
10+
use TypeError;
11+
use ValueError;
12+
13+
use function Tempest\Database\query;
14+
15+
final class DatabaseTester
16+
{
17+
public function __construct(
18+
private Container $container,
19+
) {}
20+
21+
/**
22+
* Resets the database by dropping all tables and re-running all migrations.
23+
*
24+
* @alias `reset()`
25+
*/
26+
public function setup(bool $migrate = true): self
27+
{
28+
return $this->reset($migrate);
29+
}
30+
31+
/**
32+
* Resets the database by dropping all tables and re-running all migrations.
33+
*/
34+
public function reset(bool $migrate = true): self
35+
{
36+
$migrationManager = $this->container->get(MigrationManager::class);
37+
$migrationManager->dropAll();
38+
39+
if ($migrate) {
40+
$this->migrate();
41+
}
42+
43+
return $this;
44+
}
45+
46+
/**
47+
* Migrates the specified migration classes. If no migration is specified, all application migrations will be run.
48+
*/
49+
public function migrate(string|object ...$migrationClasses): void
50+
{
51+
$migrationManager = $this->container->get(MigrationManager::class);
52+
53+
if (count($migrationClasses) === 0) {
54+
$migrationManager->up();
55+
return;
56+
}
57+
58+
foreach ($migrationClasses as $migrationClass) {
59+
$migration = is_string($migrationClass) ? $this->container->get($migrationClass) : $migrationClass;
60+
61+
$migrationManager->executeUp($migration);
62+
}
63+
}
64+
65+
/**
66+
* Asserts that a row exists in the given table matching the provided data.
67+
*/
68+
public function assertTableHasRow(string $table, mixed ...$data): void
69+
{
70+
$select = query($table)->count();
71+
72+
foreach ($data as $key => $value) {
73+
$select->whereField($key, $value);
74+
}
75+
76+
Assert::assertTrue($select->execute() > 0, "Failed asserting that a row in the table [{$table}] matches the given data.");
77+
}
78+
79+
/**
80+
* Asserts that the given table contains the specified number of rows.
81+
*/
82+
public function assertTableHasCount(string $table, int $count): void
83+
{
84+
Assert::assertSame(
85+
expected: $count,
86+
actual: query($table)->count()->execute(),
87+
message: "Failed asserting that the table [{$table}] contains [{$count}] rows.",
88+
);
89+
}
90+
91+
/**
92+
* Asserts that there is no row in the given table matching the provided data.
93+
*/
94+
public function assertTableDoesNotHaveRow(string $table, mixed ...$data): void
95+
{
96+
$select = query($table)->count();
97+
98+
foreach ($data as $key => $value) {
99+
$select->whereField($key, $value);
100+
}
101+
102+
Assert::assertTrue($select->execute() === 0, "Failed asserting that no row in the table [{$table}] matches the given data.");
103+
}
104+
105+
/**
106+
* Asserts that the given table is empty.
107+
*/
108+
public function assertTableEmpty(string $table): void
109+
{
110+
$this->assertTableHasCount($table, count: 0);
111+
}
112+
113+
/**
114+
* Asserts that the given table is not empty.
115+
*/
116+
public function assertTableNotEmpty(string $table): void
117+
{
118+
$this->assertTableHasRow($table);
119+
}
120+
}

src/Tempest/Framework/Testing/IntegrationTest.php

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Tempest\Core\Kernel;
2323
use Tempest\Database\Migrations\CreateMigrationsTable;
2424
use Tempest\Database\Migrations\MigrationManager;
25+
use Tempest\Database\Testing\DatabaseTester;
2526
use Tempest\DateTime\DateTimeInterface;
2627
use Tempest\Discovery\DiscoveryLocation;
2728
use Tempest\EventBus\EventBus;
@@ -77,6 +78,8 @@ abstract class IntegrationTest extends TestCase
7778

7879
protected OAuthTester $oauth;
7980

81+
protected DatabaseTester $database;
82+
8083
protected function setUp(): void
8184
{
8285
parent::setUp();
@@ -95,13 +98,10 @@ protected function discoverTestLocations(): array
9598
{
9699
$discoveryLocations = [];
97100

98-
$fixturesPath = to_absolute_path($this->root, 'tests/Fixtures');
101+
$testsPath = to_absolute_path($this->root, 'tests');
99102

100-
if (is_dir($fixturesPath)) {
101-
$discoveryLocations[] = new DiscoveryLocation(
102-
'Tests\\Fixtures',
103-
$fixturesPath,
104-
);
103+
if (is_dir($testsPath)) {
104+
$discoveryLocations[] = new DiscoveryLocation('Tests', $testsPath);
105105
}
106106

107107
return $discoveryLocations;
@@ -157,6 +157,7 @@ protected function setupTesters(): self
157157
$this->vite->clearCaches();
158158

159159
$this->oauth = new OAuthTester($this->container);
160+
$this->database = new DatabaseTester($this->container);
160161

161162
return $this;
162163
}
@@ -170,6 +171,11 @@ protected function setupBaseRequest(): self
170171
return $this;
171172
}
172173

174+
/**
175+
* Cleans up the database and migrates the migrations using `migrateDatabase`.
176+
*
177+
* @deprecated Use `$this->database->setup()` instead.
178+
*/
173179
protected function setupDatabase(): self
174180
{
175181
$migrationManager = $this->container->get(MigrationManager::class);
@@ -182,6 +188,8 @@ protected function setupDatabase(): self
182188

183189
/**
184190
* Creates the migration table. You may override this method to provide more migrations to run for every tests in this file.
191+
*
192+
* @deprecated Use `$this->database->migrate()` instead.
185193
*/
186194
protected function migrateDatabase(): void
187195
{
@@ -190,16 +198,12 @@ protected function migrateDatabase(): void
190198

191199
/**
192200
* Migrates the specified migration classes.
201+
*
202+
* @deprecated Use `$this->database->migrate()` instead.
193203
*/
194204
protected function migrate(string|object ...$migrationClasses): void
195205
{
196-
$migrationManager = $this->container->get(MigrationManager::class);
197-
198-
foreach ($migrationClasses as $migrationClass) {
199-
$migration = is_string($migrationClass) ? $this->container->get($migrationClass) : $migrationClass;
200-
201-
$migrationManager->executeUp($migration);
202-
}
206+
$this->database->migrate(...$migrationClasses);
203207
}
204208

205209
protected function clock(DateTimeInterface|string $now = 'now'): MockClock
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Database\Testing;
4+
5+
use PHPUnit\Framework\Assert;
6+
use PHPUnit\Framework\AssertionFailedError;
7+
use PHPUnit\Framework\Attributes\PreCondition;
8+
use PHPUnit\Framework\Attributes\Test;
9+
use Tempest\Database\MigratesUp;
10+
use Tempest\Database\Migrations\CreateMigrationsTable;
11+
use Tempest\Database\PrimaryKey;
12+
use Tempest\Database\QueryStatement;
13+
use Tempest\Database\QueryStatements\CreateTableStatement;
14+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
15+
16+
use function Tempest\Database\query;
17+
18+
/**
19+
* @mago-expect lint:no-empty-catch-clause
20+
*/
21+
final class DatabaseTesterTest extends FrameworkIntegrationTestCase
22+
{
23+
#[PreCondition]
24+
protected function configure(): void
25+
{
26+
$this->database->reset(migrate: false);
27+
$this->database->migrate(CreateMigrationsTable::class, CreateUsersTable::class);
28+
}
29+
30+
#[Test]
31+
public function assert_has_row(): void
32+
{
33+
query(User::class)
34+
->insert(name: 'Frieren', email: '[email protected]')
35+
->execute();
36+
37+
$this->database->assertTableHasRow(User::class, name: 'Frieren');
38+
$this->database->assertTableHasRow(User::class, email: '[email protected]');
39+
40+
try {
41+
$this->database->assertTableHasRow(User::class, name: 'Eisen');
42+
Assert::fail('Expected an assertion failure.');
43+
} catch (AssertionFailedError) {
44+
}
45+
}
46+
47+
#[Test]
48+
public function assert_count(): void
49+
{
50+
$this->database->assertTableHasCount(User::class, count: 0);
51+
52+
query(User::class)
53+
->insert(name: 'Frieren', email: '[email protected]')
54+
->execute();
55+
56+
query(User::class)
57+
->insert(name: 'Eisen', email: '[email protected]')
58+
->execute();
59+
60+
$this->database->assertTableHasCount(User::class, count: 2);
61+
62+
try {
63+
$this->database->assertTableHasCount(User::class, count: 3);
64+
Assert::fail('Expected an assertion failure.');
65+
} catch (AssertionFailedError) {
66+
}
67+
}
68+
69+
#[Test]
70+
public function assert_no_row(): void
71+
{
72+
query(User::class)
73+
->insert(name: 'Frieren', email: '[email protected]')
74+
->execute();
75+
76+
$this->database->assertTableDoesNotHaveRow(User::class, name: 'Eisen');
77+
78+
query(User::class)
79+
->insert(name: 'Eisen', email: '[email protected]')
80+
->execute();
81+
82+
try {
83+
$this->database->assertTableDoesNotHaveRow(User::class, name: 'Eisen');
84+
Assert::fail('Expected an assertion failure.');
85+
} catch (AssertionFailedError) {
86+
}
87+
}
88+
89+
#[Test]
90+
public function assert_table_empty(): void
91+
{
92+
$this->database->assertTableEmpty(User::class);
93+
94+
query(User::class)
95+
->insert(name: 'Frieren', email: '[email protected]')
96+
->execute();
97+
98+
try {
99+
$this->database->assertTableEmpty(User::class);
100+
Assert::fail('Expected an assertion failure.');
101+
} catch (AssertionFailedError) {
102+
}
103+
}
104+
105+
#[Test]
106+
public function assert_table_not_empty(): void
107+
{
108+
try {
109+
$this->database->assertTableNotEmpty(User::class);
110+
Assert::fail('Expected an assertion failure.');
111+
} catch (AssertionFailedError) {
112+
}
113+
114+
query(User::class)
115+
->insert(name: 'Frieren', email: '[email protected]')
116+
->execute();
117+
118+
$this->database->assertTableNotEmpty(User::class);
119+
}
120+
}
121+
122+
final class User
123+
{
124+
public PrimaryKey $id;
125+
126+
public string $name;
127+
128+
public string $email;
129+
}
130+
131+
final class CreateUsersTable implements MigratesUp
132+
{
133+
public string $name = '0-create_users_table';
134+
135+
public function up(): QueryStatement
136+
{
137+
return new CreateTableStatement('users')
138+
->primary()
139+
->string('name')
140+
->string('email');
141+
}
142+
}

tests/Integration/FrameworkIntegrationTestCase.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace Tests\Tempest\Integration;
66

77
use InvalidArgumentException;
8-
use PHPUnit\Framework\Assert;
98
use Stringable;
109
use Tempest\Database\DatabaseInitializer;
1110
use Tempest\Database\Migrations\MigrationManager;

0 commit comments

Comments
 (0)