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.
./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());
}
}
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
paratestor PHPUnit's process isolation. - Environment separation: Create a separate
.env.testingfile for test‑specific settings (e.g., database credentials, mail drivers). - Custom assertions: Extend
TestResponseto 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 usingRefreshesDatabase. You can also call it insetUp()or in a@beforeannotation. - 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.