0

TestCase base class — bootstrapping the framework for tests

Intermediate5 min read·fw-11-001

Concept

TestCase base class bootstraps the framework for tests. It creates a test application instance, handles database setup, and provides the HTTP testing helpers. All test classes extend it.

What the TestCase does:

  1. Boots the application (container bindings, service providers).
  2. Configures the test environment (SQLite in-memory, fake mail, test config).
  3. Provides setUp() and tearDown() hooks.
  4. Exposes HTTP testing methods (get(), post(), etc.).
  5. Manages database transactions or fresh migrations per test.

createApplication() method: Returns the booted application instance. Called once per test class (or once per test). Mirrors the production bootstrap but with test-specific configuration (in-memory SQLite, no real mail, etc.).

Environment override: Load .env.testing instead of .env. Configure: DB_CONNECTION=sqlite, DB_DATABASE=:memory:, MAIL_MAILER=array (no real mail), QUEUE_CONNECTION=sync.

RefreshDatabase implementation: In setUp(), wrap the test in a DB transaction (beginTransaction()). In tearDown(), roll back (rollBack()). This is fast because no actual migration runs per test — just transaction rollback. For first run, run migrate.

createApplication() caching: The application is created ONCE per test run (process-level static instance). Each test operates within a transaction that rolls back. This avoids re-booting the container for every test method.

Code Example

php
<?php
namespace Framework\Testing;

use Framework\Foundation\Application;
use Framework\Database\Connection;
use PHPUnit\Framework\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    protected static ?Application $app = null;
    protected static bool $migrated    = false;

    protected function setUp(): void
    {
        parent::setUp();
        if (static::$app === null) {
            static::$app = $this->createApplication();
        }
        if ($this->usesRefreshDatabase() && !static::$migrated) {
            $this->runMigrations();
            static::$migrated = true;
        }
        if ($this->usesRefreshDatabase()) {
            $this->getConnection()->beginTransaction(); // wrap test in transaction
        }
    }

    protected function tearDown(): void
    {
        if ($this->usesRefreshDatabase()) {
            $this->getConnection()->rollBack(); // rollback after each test
        }
        parent::tearDown();
    }

    protected function createApplication(): Application
    {
        // Load test environment
        $app = new Application(base_path());
        $app->loadEnvironment('.env.testing');
        $app->registerServiceProviders();
        return $app;
    }

    private function runMigrations(): void
    {
        $migrator = static::$app->make(\Framework\Console\Commands\Migrator::class);
        foreach ($migrator->getPending() as $file) {
            $migrator->run($file);
        }
    }

    protected function getConnection(): Connection
    {
        return static::$app->make(Connection::class);
    }

    private function usesRefreshDatabase(): bool
    {
        // Check if the current test class uses RefreshDatabase trait
        return in_array(RefreshDatabase::class, class_uses_recursive(static::class));
    }

    // Container resolution helpers
    protected function app(?string $abstract = null): mixed
    {
        return $abstract ? static::$app->make($abstract) : static::$app;
    }

    // Override bindings for tests
    protected function swap(string $abstract, mixed $instance): void
    {
        static::$app->instance($abstract, $instance);
    }
}