Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 50 additions & 81 deletions docs/1-essentials/07-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,125 +6,73 @@ keywords: ["phpunit", "pest"]

## Overview

Tempest uses [PHPUnit](https://phpunit.de) for testing and provides an integration through the [`Tempest\Framework\Testing\IntegrationTest`](https://github.com/tempestphp/tempest-framework/blob/main/src/Tempest/Framework/Testing/IntegrationTest.php) test case. This class boots the framework with configuration suitable for testing, and provides access to multiple utilities.
Tempest uses [PHPUnit](https://phpunit.de) for testing and provides an integration through the [`IntegrationTest`](https://github.com/tempestphp/tempest-framework/blob/main/src/Tempest/Framework/Testing/IntegrationTest.php) test case. This class boots the framework with configuration suitable for testing, and provides access to multiple utilities.

Testing utilities specific to components are documented in their respective chapters. For instance, testing the router is described in the [routing documentation](./01-routing.md#testing).

## Running tests

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`.
Any test class that needs to interact with Tempest must extend [`IntegrationTest`](https://github.com/tempestphp/tempest-framework/blob/main/src/Tempest/Framework/Testing/IntegrationTest.php).

Running the test suite is done by running `composer phpunit`.
By default, Tempest ships with a `phpunit.xml` file that configures PHPUnit to find test files in the `tests` directory. You may run tests using the following command:

```sh
composer phpunit
```

## Test-specific discovery locations

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.

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).

```php tests/HomeControllerTest.php
use Tempest\Core\DiscoveryLocation;
use Tempest\Framework\Testing\IntegrationTest;

final class HomeControllerTest extends IntegrationTest
{
protected function discoverTestLocations(): array
{
return [
new DiscoveryLocation('Tests\\OtherFixtures', __DIR__ . '/OtherFixtures'),
];
}
}
./vendor/bin/phpunit
```

## Using the database

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.
By default, tests don't interact with the database. You may manually set up the database for testing in test files by using the `setup()` method on the `database` testing utility.

```php
final class TodoControllerTest extends IntegrationTest
```php tests/ShowAircraftControllerTest.php
final class ShowAircraftControllerTest extends IntegrationTest
{
protected function setUp(): void
#[PreCondition]
protected function configure(): void
{
parent::setUp();

$this->setupDatabase();
$this->database->setup();
}
}
```

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:
:::info
The [`PreCondition`](https://docs.phpunit.de/en/12.5/attributes.html#precondition) attribute instructs PHPUnit to run the associated method after the `setUp()` method. We recommend using it instead of overriding `setUp()` directly.
:::

```php tests/Fixtures/database.config.php
use Tempest\Database\Config\SQLiteConfig;
### Runnig migrations

return new SQLiteConfig(
path: __DIR__ . '/database-testing.sqlite'
);
```

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.

```php
final class TodoControllerTest extends IntegrationTest
{
protected function migrateDatabase(): void
{
$this->migrate(
CreateMigrationsTable::class,
CreateTodosTable::class,
);
}
}
```
By default, all migrations are run when setting up the database. However, you may choose to run only specific migrations by using the `migrate()` method instead of `setup()`.

```php
final class TodoControllerTest extends IntegrationTest
```php tests/ShowAircraftControllerTest.php
final class ShowAircraftControllerTest extends IntegrationTest
{
public function test_create_todo(): void
#[Test]
public function shows_aircraft(): void
{
$this->migrate(
$this->database->migrate(
CreateMigrationsTable::class,
CreateTodosTable::class,
CreateAircraftTable::class,
);

// …
}
}
```

## Tester utilities

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:
### Using a dedicated testing database

```php
$this->http
->get('/account/profile')
->assertOk()
->assertSee('My Profile');
```
To ensure your tests run in isolation and do not affect your main database, you may configure a dedicated test database connection.

There's the [console tester](../1-essentials/04-console-commands.md#testing):
To do so, create a `database.testing.config.php` file anywhere—Tempest will [use it](./06-configuration.md#per-environment-configuration) to override the default database settings.

```php tests/ExportUsersCommandTest.php
$this->console
->call(ExportUsersCommand::class)
->assertSuccess()
->assertSee('12 users exported');
```php tests/database.testing.config.php
use Tempest\Database\Config\SQLiteConfig;

$this->console
->call(WipeDatabaseCommand::class)
->assertSee('caution')
->submit()
->assertSuccess();
return new SQLiteConfig(
path: __DIR__ . '/testing.sqlite'
);
```

And many, many more.

## Spoofing the environment

By default, Tempest provides a `phpunit.xml` that sets the `ENVIRONMENT` variable to `testing`. This is needed so that Tempest can adapt its boot process and load the proper configuration files for the testing environment.
Expand Down Expand Up @@ -152,6 +100,27 @@ For instance, you may colocate test files and their corresponding class by chang
</testsuites>
```

## Discovering test-specific fixtures

Non-test files created in the `tests` directory are automatically discovered by Tempest when running the test suite.

You can override this behavior by providing your own implementation of `discoverTestLocations()`:

```php tests/Aircraft/ShowAircraftControllerTest.php
use Tempest\Core\DiscoveryLocation;
use Tempest\Framework\Testing\IntegrationTest;

final class ShowAircraftControllerTest extends IntegrationTest
{
protected function discoverTestLocations(): array
{
return [
new DiscoveryLocation('Tests\\Aircraft', __DIR__ . '/Aircraft'),
];
}
}
```

## Using Pest as a test runner

[Pest](https://pestphp.com/) is a test runner built on top of PHPUnit. It provides a functional way of writing tests similar to JavaScript testing frameworks like [Vitest](https://vitest.dev/), and features an elegant console reporter.
Expand Down
70 changes: 33 additions & 37 deletions src/Tempest/Framework/Testing/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
use Tempest\Core\ExceptionTester;
use Tempest\Core\FrameworkKernel;
use Tempest\Core\Kernel;
use Tempest\Database\Migrations\CreateMigrationsTable;
use Tempest\Database\Migrations\MigrationManager;
use Tempest\Database\Testing\DatabaseTester;
use Tempest\DateTime\DateTimeInterface;
use Tempest\Discovery\DiscoveryLocation;
Expand Down Expand Up @@ -57,26 +55,59 @@ abstract class IntegrationTest extends TestCase

protected ConsoleTester $console;

/**
* Provides utilities for testing HTTP routes.
*/
protected HttpRouterTester $http;

/**
* Provides utilities for testing installers.
*/
protected InstallerTester $installer;

/**
* Provides utilities for testing the Vite integration.
*/
protected ViteTester $vite;

/**
* Provides utilities for testing the event bus.
*/
protected EventBusTester $eventBus;

/**
* Provides utilities for testing storage management.
*/
protected StorageTester $storage;

/**
* Provides utilities for testing emails.
*/
protected MailTester $mailer;

/**
* Provides utilities for testing the cache.
*/
protected CacheTester $cache;

/**
* Provides utilities for testing exception reporting.
*/
protected ExceptionTester $exceptions;

/**
* Provides utilities for testing process execution.
*/
protected ProcessTester $process;

/**
* Provides utilities for testing OAuth flows.
*/
protected OAuthTester $oauth;

/**
* Provides utilities for testing the database.
*/
protected DatabaseTester $database;

protected function setUp(): void
Expand Down Expand Up @@ -170,41 +201,6 @@ protected function setupBaseRequest(): self
return $this;
}

/**
* Cleans up the database and migrates the migrations using `migrateDatabase`.
*
* @deprecated Use `$this->database->setup()` instead.
*/
protected function setupDatabase(): self
{
$migrationManager = $this->container->get(MigrationManager::class);
$migrationManager->dropAll();

$this->migrateDatabase();

return $this;
}

/**
* Creates the migration table. You may override this method to provide more migrations to run for every tests in this file.
*
* @deprecated Use `$this->database->migrate()` instead.
*/
protected function migrateDatabase(): void
{
$this->migrate(CreateMigrationsTable::class);
}

/**
* Migrates the specified migration classes.
*
* @deprecated Use `$this->database->migrate()` instead.
*/
protected function migrate(string|object ...$migrationClasses): void
{
$this->database->migrate(...$migrationClasses);
}

protected function clock(DateTimeInterface|string $now = 'now'): MockClock
{
$clock = new MockClock($now);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class CurrentAuthenticatableTest extends FrameworkIntegrationTestCase
#[PreCondition]
protected function configure(): void
{
$this->migrate(CreateMigrationsTable::class, CreateServiceAccountTableMigration::class);
$this->database->migrate(CreateMigrationsTable::class, CreateServiceAccountTableMigration::class);

$this->container->config(new AuthConfig(authenticatables: [ServiceAccount::class]));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ final class DatabaseAuthenticatableResolverTest extends FrameworkIntegrationTest
#[Test]
public function can_resolve_custom_authenticatable_class(): void
{
$this->migrate(CreateMigrationsTable::class, CreateApiTokensTableMigration::class);
$this->database->migrate(CreateMigrationsTable::class, CreateApiTokensTableMigration::class);

$this->container->config(new AuthConfig(authenticatables: [ApiToken::class]));

Expand All @@ -37,7 +37,7 @@ public function can_resolve_custom_authenticatable_class(): void
#[Test]
public function can_resolve_id_from_custom_authenticatable_class(): void
{
$this->migrate(CreateMigrationsTable::class, CreateApiTokensTableMigration::class);
$this->database->migrate(CreateMigrationsTable::class, CreateApiTokensTableMigration::class);

$this->container->config(new AuthConfig(authenticatables: [ApiToken::class]));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ protected function configure(): void
$this->container->get(SessionConfig::class),
));

$this->migrate(CreateMigrationsTable::class, CreateUsersTableMigration::class, CreateApiKeysTableMigration::class);
$this->database->migrate(CreateMigrationsTable::class, CreateUsersTableMigration::class, CreateApiKeysTableMigration::class);
}

#[PostCondition]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public function test_count_query_with_conditions(): void

public function test_count(): void
{
$this->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class);
$this->database->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class);

query('authors')
->insert(
Expand Down
6 changes: 3 additions & 3 deletions tests/Integration/Database/Builder/CustomPrimaryKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class CustomPrimaryKeyTest extends FrameworkIntegrationTestCase
{
public function test_model_with_custom_primary_key_name(): void
{
$this->migrate(CreateMigrationsTable::class, CreateCustomPrimaryKeyUserModelTable::class);
$this->database->migrate(CreateMigrationsTable::class, CreateCustomPrimaryKeyUserModelTable::class);

$frieren = query(CustomPrimaryKeyUserModel::class)->create(name: 'Frieren', magic: 'Time Magic');

Expand All @@ -32,7 +32,7 @@ public function test_model_with_custom_primary_key_name(): void

public function test_update_or_create_with_custom_primary_key(): void
{
$this->migrate(CreateMigrationsTable::class, CreateCustomPrimaryKeyUserModelTable::class);
$this->database->migrate(CreateMigrationsTable::class, CreateCustomPrimaryKeyUserModelTable::class);

$frieren = query(CustomPrimaryKeyUserModel::class)->create(name: 'Frieren', magic: 'Time Magic');

Expand All @@ -47,7 +47,7 @@ public function test_update_or_create_with_custom_primary_key(): void

public function test_model_without_id_property_still_works(): void
{
$this->migrate(CreateMigrationsTable::class, CreateModelWithoutIdMigration::class);
$this->database->migrate(CreateMigrationsTable::class, CreateModelWithoutIdMigration::class);

$model = query(ModelWithoutId::class)->new(name: 'Test');
$this->assertInstanceOf(ModelWithoutId::class, $model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public function test_delete_on_plain_table_with_conditions(): void

public function test_delete_with_non_object_model(): void
{
$this->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class);
$this->database->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class);

query('authors')
->insert(
Expand Down
4 changes: 2 additions & 2 deletions tests/Integration/Database/Builder/InsertQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public function test_insert_on_model_table_with_existing_relation(): void

public function test_then_method(): void
{
$this->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class, CreateBookTable::class, CreateChapterTable::class);
$this->database->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class, CreateBookTable::class, CreateChapterTable::class);

$id = query(Book::class)
->insert(title: 'Timeline Taxi')
Expand All @@ -154,7 +154,7 @@ public function test_then_method(): void

public function test_insert_with_non_object_model(): void
{
$this->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class);
$this->database->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class);

query('authors')
->insert(
Expand Down
Loading