Skip to content

Commit 830e27b

Browse files
authored
feat(core): improve base testing class (#1509)
1 parent af512b0 commit 830e27b

File tree

3 files changed

+197
-97
lines changed

3 files changed

+197
-97
lines changed

docs/1-essentials/07-testing.md

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,56 +12,121 @@ Testing utilities specific to components are documented in their respective chap
1212

1313
## Running tests
1414

15-
If you created a Tempest application through the [recommended installation process](../0-getting-started/02-installation.md), you already have access to `tests/IntegrationTestCase`, which your application tests can inherit from.
15+
Any test class that wants to interact with Tempest should extend from [`IntegrationTest`](https://github.com/tempestphp/tempest-framework/blob/main/src/Tempest/Framework/Testing/IntegrationTest.php). Next, any test class should end with the suffix `Test`.
1616

17-
In this case, you may use the `composer phpunit` command to run your test suite.
17+
Running the test suite is done by running `composer phpunit`.
1818

1919
```sh
2020
composer phpunit
2121
```
2222

23-
## Creating new test files
23+
## Test-specific discovery locations
24+
25+
Tempest will only discover non-dev namespaces defined in composer.json automatically. That means that `{:hl-keyword:require-dev:}` namespaces aren't discovered automatically. Whenever you need Tempest to discover test-specific locations, you may specify them within the `discoverTestLocations()` method of the provided `IntegrationTest` class.
2426

25-
By default, PHPUnit is configured to look for test files that end in `*Test.php` in the root `tests` directory. You may create a such a file and make it extend `IntegrationTestCase`.
27+
On top of that, Tempest _will_ look for files in the `tests/Fixtures` directory and discover them by default. You can override this behavior by providing your own implementation of `discoverTestLocations()`, where you can return an array of `DiscoveryLocation` objects (or nothing).
2628

2729
```php tests/HomeControllerTest.php
28-
use Tests\IntegrationTestCase;
30+
use Tempest\Core\DiscoveryLocation;
31+
use Tempest\Framework\Testing\IntegrationTest;
2932

30-
final class HomeControllerTest extends IntegrationTestCase
33+
final class HomeControllerTest extends IntegrationTest
3134
{
32-
public function test_index(): void
35+
protected function discoverTestLocations(): array
3336
{
34-
$this->http
35-
->get('/')
36-
->assertOk();
37+
return [
38+
new DiscoveryLocation('Tests\\OtherFixtures', __DIR__ . '/OtherFixtures'),
39+
];
3740
}
3841
}
3942
```
4043

41-
## Test-specific discovery locations
44+
## Using the database
4245

43-
Tempest does not discover files outside of the namespaces defined in the `require` object of `composer.json`. If you need Tempest to discover test-specific fixture files, you may specify paths using the `discoveryLocations` property of the provided `IntegrationTestCase` class.
46+
If you want to test code that interacts with the database, your test class can call the `setupDatabase()` method. This method will create and migrate a clean database for you on the fly.
4447

45-
For instance, you may create a `tests/config` directory that contains test-specific configuration files, and instruct Tempest to discover them:
48+
```php
49+
class TodoControllerTest extends IntegrationTest
50+
{
51+
protected function setUp(): void
52+
{
53+
parent::setUp();
4654

47-
```php tests/IntegrationTestCase.php
48-
use Tempest\Core\DiscoveryLocation;
55+
$this->setupDatabase();
56+
}
57+
}
58+
```
4959

50-
final class IntegrationTestCase extends TestCase
51-
{
52-
protected string $root = __DIR__ . '/../';
60+
Most likely, you'll want to use a test-specific database connection. You can create a `database.config.php` file anywhere within test-specific discovery locations, and Tempest will use that connection instead of the project's default. For example, you can create a file `tests/Fixtures/database.config.php` like so:
5361

54-
protected function setUp(): void
62+
```php tests/Fixtures/database.config.php
63+
<?php
64+
65+
use Tempest\Database\Config\SQLiteConfig;
66+
67+
return new SQLiteConfig(
68+
path: __DIR__ . '/database-testing.sqlite'
69+
);
70+
```
71+
72+
By default, no tables will be migrated. You can choose to provide a list of migrations that will be run for every test that calls `setupDatabase()`, or you can run specific migrations on a per-test basis.
73+
74+
```php
75+
class TodoControllerTest extends IntegrationTest
76+
{
77+
protected function migrateDatabase(): void
5578
{
56-
$this->discoveryLocations = [
57-
new DiscoveryLocation(namespace: 'Tests\\Config', path: __DIR__ . '/config'),
58-
];
79+
$this->migrate(
80+
CreateMigrationsTable::class,
81+
CreateTodosTable::class,
82+
);
83+
}
84+
}
85+
```
5986

60-
parent::setUp();
87+
```php
88+
class TodoControllerTest extends IntegrationTest
89+
{
90+
public function test_create_todo(): void
91+
{
92+
$this->migrate(
93+
CreateMigrationsTable::class,
94+
CreateTodosTable::class,
95+
);
96+
97+
// …
6198
}
6299
}
63100
```
64101

102+
## Tester utilities
103+
104+
The `IntegrationTest` provides several utilities to make testing easier. You can read the details about each tester utility on the documentation page of its respective component. For example, there's the [http tester](../1-essentials/01-routing.md#testing) that helps you test HTTP requests:
105+
106+
```php
107+
$this->http
108+
->get('/account/profile')
109+
->assertOk()
110+
->assertSee('My Profile');
111+
```
112+
113+
There's the [console tester](../1-essentials/04-console-commands.md#testing):
114+
115+
```php tests/ExportUsersCommandTest.php
116+
$this->console
117+
->call(ExportUsersCommand::class)
118+
->assertSuccess()
119+
->assertSee('12 users exported');
120+
121+
$this->console
122+
->call(WipeDatabaseCommand::class)
123+
->assertSee('caution')
124+
->submit()
125+
->assertSuccess();
126+
```
127+
128+
And many, many more.
129+
65130
## Changing the location of tests
66131

67132
The `phpunit.xml` file contains a `{html}<testsuite>` element that configures the directory in which PHPUnit looks for test files. This may be changed to follow any rule of your convenience.
@@ -92,7 +157,7 @@ The next step is to create a `tests/Pest.php` file, which will instruct Pest how
92157

93158
```php tests/Pest.php
94159
pest()
95-
->extend(Tests\IntegrationTestCase::class)
160+
->extend(Tests\IntegrationTest::class)
96161
->in(__DIR__);
97162
```
98163

src/Tempest/Framework/Testing/IntegrationTest.php

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,39 @@
44

55
namespace Tempest\Framework\Testing;
66

7+
use Closure;
78
use PHPUnit\Framework\TestCase;
89
use Tempest\Cache\Testing\CacheTester;
910
use Tempest\Clock\Clock;
1011
use Tempest\Clock\MockClock;
12+
use Tempest\Console\Output\MemoryOutputBuffer;
13+
use Tempest\Console\Output\StdoutOutputBuffer;
14+
use Tempest\Console\OutputBuffer;
1115
use Tempest\Console\Testing\ConsoleTester;
1216
use Tempest\Container\GenericContainer;
1317
use Tempest\Core\AppConfig;
1418
use Tempest\Core\ExceptionTester;
1519
use Tempest\Core\FrameworkKernel;
1620
use Tempest\Core\Kernel;
21+
use Tempest\Core\ShellExecutor;
22+
use Tempest\Core\ShellExecutors\NullShellExecutor;
23+
use Tempest\Database\Migrations\CreateMigrationsTable;
1724
use Tempest\Database\Migrations\MigrationManager;
1825
use Tempest\DateTime\DateTimeInterface;
26+
use Tempest\Discovery\DiscoveryLocation;
1927
use Tempest\EventBus\EventBus;
2028
use Tempest\EventBus\Testing\EventBusTester;
2129
use Tempest\Framework\Testing\Http\HttpRouterTester;
2230
use Tempest\Http\GenericRequest;
2331
use Tempest\Http\Method;
2432
use Tempest\Http\Request;
25-
use Tempest\Mail\MailerConfig;
2633
use Tempest\Mail\Testing\MailTester;
2734
use Tempest\Mail\Testing\TestingMailer;
2835
use Tempest\Storage\Testing\StorageTester;
29-
use Tempest\View\ViewRenderer;
36+
use Throwable;
3037

3138
use function Tempest\Support\Path\normalize;
39+
use function Tempest\Support\Path\to_absolute_path;
3240

3341
/** @mago-expect maintainability/too-many-properties */
3442
abstract class IntegrationTest extends TestCase
@@ -66,19 +74,63 @@ protected function setUp(): void
6674
{
6775
parent::setUp();
6876

77+
$this->setupKernel()
78+
->setupConsole()
79+
->setupTesters()
80+
->setupBaseRequest();
81+
}
82+
83+
/**
84+
* Returns an array of DiscoveryLocations that should be discovered only during testing
85+
* @return \Tempest\Discovery\DiscoveryLocation[]
86+
*/
87+
protected function discoverTestLocations(): array
88+
{
89+
$discoveryLocations = [];
90+
91+
$fixturesPath = to_absolute_path($this->root, 'tests/Fixtures');
92+
93+
if (is_dir($fixturesPath)) {
94+
$discoveryLocations[] = new DiscoveryLocation(
95+
'Tests\\Fixtures',
96+
$fixturesPath,
97+
);
98+
}
99+
100+
return $discoveryLocations;
101+
}
102+
103+
protected function setupKernel(): self
104+
{
69105
// We force forward slashes for consistency even on Windows.
70-
$this->root ??= normalize(realpath(__DIR__ . '/../../'));
106+
$this->root ??= normalize(realpath(getcwd()));
107+
108+
$discoveryLocations = [...$this->discoveryLocations, ...$this->discoverTestLocations()];
71109

72110
$this->kernel ??= FrameworkKernel::boot(
73111
root: $this->root,
74-
discoveryLocations: $this->discoveryLocations,
112+
discoveryLocations: $discoveryLocations,
75113
);
76114

77115
/** @var GenericContainer $container */
78116
$container = $this->kernel->container;
79117
$this->container = $container;
80118

119+
return $this;
120+
}
121+
122+
protected function setupConsole(): self
123+
{
81124
$this->console = new ConsoleTester($this->container);
125+
$this->container->singleton(OutputBuffer::class, fn () => new MemoryOutputBuffer());
126+
$this->container->singleton(StdoutOutputBuffer::class, fn () => new MemoryOutputBuffer());
127+
$this->container->singleton(ShellExecutor::class, fn () => new NullShellExecutor());
128+
129+
return $this;
130+
}
131+
132+
protected function setupTesters(): self
133+
{
82134
$this->http = new HttpRouterTester($this->container);
83135
$this->installer = new InstallerTester($this->container);
84136
$this->eventBus = new EventBusTester($this->container);
@@ -95,9 +147,34 @@ protected function setUp(): void
95147
$this->vite->preventTagResolution();
96148
$this->vite->clearCaches();
97149

150+
return $this;
151+
}
152+
153+
protected function setupBaseRequest(): self
154+
{
98155
$request = new GenericRequest(Method::GET, '/', []);
99156
$this->container->singleton(Request::class, fn () => $request);
100157
$this->container->singleton(GenericRequest::class, fn () => $request);
158+
159+
return $this;
160+
}
161+
162+
protected function setupDatabase(): self
163+
{
164+
$migrationManager = $this->container->get(MigrationManager::class);
165+
166+
$migrationManager->dropAll();
167+
168+
$this->migrateDatabase();
169+
170+
return $this;
171+
}
172+
173+
protected function migrateDatabase(): void
174+
{
175+
$this->migrate(
176+
CreateMigrationsTable::class,
177+
);
101178
}
102179

103180
protected function migrate(string|object ...$migrationClasses): void
@@ -139,4 +216,24 @@ protected function tearDown(): void
139216
/** @phpstan-ignore-next-line */
140217
unset($this->http);
141218
}
219+
220+
protected function assertException(
221+
string $expectedExceptionClass,
222+
Closure $handler,
223+
?Closure $assertException = null,
224+
): void {
225+
try {
226+
$handler();
227+
} catch (Throwable $throwable) {
228+
$this->assertInstanceOf($expectedExceptionClass, $throwable);
229+
230+
if ($assertException !== null) {
231+
$assertException($throwable);
232+
}
233+
234+
return;
235+
}
236+
237+
$this->fail("Expected exception {$expectedExceptionClass} was not thrown");
238+
}
142239
}

0 commit comments

Comments
 (0)