PHPUnit Testing Suite

Rhapsody ships with a ready‑to‑run testing environment built on PHPUnit. You get a robust base TestCase class that integrates with the framework's container, HTTP routing, and Doctrine ORM, plus convenient traits and mocks to speed up your feature and unit tests.

Quick Start: Run ./vendor/bin/phpunit from the project root. All tests are located in the tests/ directory.

Configuration Setup

The phpunit.xml file is already provided in the project root. It points to tests/bootstrap.php as the bootstrap file. Make sure your bootstrap.php defines the ROOT_DIR constant and loads Composer's autoloader:

<?php
// tests/bootstrap.php
define('ROOT_DIR', dirname(__DIR__));
require_once ROOT_DIR . '/vendor/autoload.php';

This file is executed before every test run, ensuring that your application's classes are autoloaded and any global constants are defined.


Base Test Case & Productivity Traits

All your test classes should extend Rhapsody\Core\Testing\TestCase. This base class automatically boots a fresh application container in setUp(), so you have access to all registered services.

1. Database Isolation via RefreshesDatabase

To prevent test data from interfering between tests, use the RefreshesDatabase trait. It provides a setUpDatabase() method that drops and recreates all tables based on your Doctrine entity metadata.

namespace Tests\Feature;

use Rhapsody\Core\Testing\TestCase;
use Rhapsody\Core\Testing\Traits\RefreshesDatabase;

class UserTest extends TestCase
{
    use RefreshesDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        $this->setUpDatabase(); // Rebuilds schema
    }

    public function test_user_can_be_created(): void
    {
        $user = new User(['email' => 'test@example.com']);
        $this->app->get('entityManager')->persist($user);
        $this->app->get('entityManager')->flush();

        $this->assertNotNull($user->getId());
    }
}
Performance Tip: For large test suites, consider using transactions instead of full rebuilds. The trait can be extended to support transaction‑based rollbacks – check the framework's advanced testing guide.

2. Fluent HTTP Response Assertions

The call() method dispatches a request through the router and returns a TestResponse object. This wrapper offers a fluent interface for common assertions, making your tests more readable.

public function test_homepage_returns_successful_response(): void
{
    $this->call('GET', '/')
         ->assertOk()                  // status 200
         ->assertSee('Welcome to Rhapsody')
         ->assertDontSee('Error');
}

public function test_login_validation_fails(): void
{
    $response = $this->call('POST', '/login', [
        'email' => 'invalid',
        'password' => 'short',
    ]);

    $response->assertStatus(302)       // redirect
             ->assertSessionHasErrors(['email', 'password']);
}

Available assertion methods include:

  • assertOk(), assertStatus($code)
  • assertSee($text), assertDontSee($text)
  • assertJson(array $data), assertJsonMissing(array $data)
  • assertRedirect($uri)
  • assertSessionHas($key, $value), assertSessionHasErrors()

3. Dependency Injection Container Mocks

Two helper methods (swap() and mock()) allow you to substitute real services with test doubles.

  • swap($abstract, $instance) – replaces the container's binding for the given interface or class with your own object.
  • mock($abstract, callable $mockDefinition) – creates a PHPUnit mock, lets you configure expectations, and then swaps it into the container in one step.
public function test_payment_flow(): void
{
    // Swap with a custom fake
    $fakeGateway = new FakePaymentGateway();
    $this->swap(PaymentGatewayInterface::class, $fakeGateway);

    // Or create a mock with expectations
    $this->mock(PaymentGatewayInterface::class, function ($mock) {
        $mock->expects($this->once())
             ->method('charge')
             ->with(100.00)
             ->willReturn(true);
    });

    $this->call('POST', '/checkout', ['amount' => 100])
         ->assertOk()
         ->assertSee('Payment Successful');
}

This approach allows you to test controller logic without hitting external APIs or relying on real implementations.


Testing with reCAPTCHA

When testing forms that include a reCAPTCHA widget, you can mock the validation rule or the Recaptcha helper to always return success. For example:

use Rhapsody\Core\Helpers\Recaptcha;

public function test_registration_with_recaptcha(): void
{
    // Make the verify() method always return true
    $this->mock(Recaptcha::class, function ($mock) {
        $mock->method('verify')->willReturn(true);
    });

    $this->call('POST', '/register', [
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'password' => 'secret123',
        'password_confirmation' => 'secret123',
        'g-recaptcha-response' => 'dummy-token', // still required, but validation passes
    ])->assertRedirect('/dashboard');
}

Alternatively, you can disable the rule entirely in your test environment by using a conditional validation rule (e.g., only apply recaptcha if APP_ENV !== 'testing').


Additional Tips

  • Test coverage: Use phpunit --coverage-html coverage/ to generate an HTML coverage report (requires Xdebug or pcov).
  • Parallel testing: For large suites, consider using paratest or PHPUnit's process isolation.
  • Environment separation: Create a separate .env.testing file for test‑specific settings (e.g., database credentials, mail drivers).
  • Custom assertions: Extend TestResponse to add your own helpers for domain‑specific responses.

Common Pitfalls

  • Container not refreshed: Each test method runs in a fresh container. Do not rely on global state between tests.
  • Database not rebuilt: Always call setUpDatabase() if using RefreshesDatabase. You can also call it in setUp() or in a @before annotation.
  • Mocking final classes: PHPUnit cannot mock final classes; use swap() with a custom implementation instead.
  • Response content missing: Ensure the router actually returns a response; if a controller throws an exception, the test will fail early.

For further details, refer to the official PHPUnit documentation and the framework's source code in Rhapsody\Core\Testing.