Alxarafe Testing Guide
Introduction
Automated testing is a fundamental pillar in development with Alxarafe. It not only ensures that the code works as expected but also acts as a safety net allowing for aggressive refactoring and serving as living documentation of the system's behavior.
Alxarafe includes a robust PHPUnit configuration integrated with Docker, designed to facilitate both unit tests (models, pure logic) and feature tests (controllers, API).
Testing Environment
Infrastructure
The testing environment is containerized and relies on the following pillars:
- Docker Container (
alxarafe_php): All tests run inside the PHP container to ensure consistency with the production environment (same extensions, PHP version 8.5+). - Isolated Database (
alxarafe_test):- When tests start, the system automatically connects to a dedicated database called
alxarafe_test. - This database is automatically created and migrated if it doesn't exist.
- Important: Tests are never run against the development or production database.
- When tests start, the system automatically connects to a dedicated database called
- Database Transactions:
- Each test is automatically wrapped in a database transaction (
beginTransactioninsetUp). - Upon test completion, the transaction is rolled back (
rollBackintearDown). - This ensures that no data persists after a test execution, keeping the environment clean for the next one.
- Each test is automatically wrapped in a database transaction (
Bootstrapping
The Tests/bootstrap.php file (and its class Tests\Bootstrapper) is responsible for:
- Defining the
ALX_TESTINGconstant. - Loading environment configurations.
- Initializing the connection to the test database.
- Executing necessary migrations.
Directory Structure
The project distinguishes between "core" (framework) tests and "application" (skeleton) tests.
Tests/: Contains the base testing infrastructure and unit tests for the Alxarafe framework itself (src/Core).Tests/TestCase.php: Base class from which all tests must inherit.Tests/Unit/: Core unit tests.Tests/Feature/: Core integration tests.
skeleton/Tests/: Contains specific tests for the example application or final project.skeleton/Tests/Unit/: Tests for your Models and Classes.skeleton/Tests/Feature/: Tests for your Controllers and APIs.
Creating Tests
All tests must inherit from Tests\TestCase. This base class provides transaction utilities and custom assertions.
1. Model Tests (Unit)
Unit tests for models verify that business logic and persistence work correctly.
Example: skeleton/Tests/Unit/PersonTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Modules\Agenda\Model\Person;
class PersonTest extends TestCase
{
/**
* Verifies that a person can be created and saved to DB.
*/
public function testItCanCreateAPerson()
{
// 1. Execution
$person = Person::create([
'name' => 'John',
'lastname' => 'Doe',
'birth_date' => '1990-01-01',
'active' => true
]);
// 2. Assertions
$this->assertNotNull($person->id, 'ID should not be null after creation');
$this->assertEquals('John', $person->name);
$this->assertTrue($person->active);
// 3. Database Verification
// (Note: This checks within the current transaction)
$this->assertDatabaseHas('people', [
'id' => $person->id,
'name' => 'John'
]);
}
}2. API and Controller Tests (Feature)
Testing controllers in Alxarafe has particularities because the framework handles HTTP responses and redirects.
Handling Responses (HttpResponseException)
In the test environment (ALX_TESTING), functions like httpRedirect or jsonResponse do not terminate execution (die()). Instead, they throw an Alxarafe\Base\Testing\HttpResponseException. This allows capturing the response and making assertions on it.
Simulating Authentication
To test protected controllers without going through the full login process, you can define ALX_TEST_USER before instantiating the controller.
Example: skeleton/Tests/Feature/PersonControllerTest.php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Modules\Agenda\Controller\PersonController;
use Alxarafe\Base\Testing\HttpResponseException;
class PersonControllerTest extends TestCase
{
/**
* Expected failure test: Validation.
*/
public function testItReturnsValidationErrorOnEmptySave()
{
// 1. Prepare Environment
$_POST = ['action' => 'save', 'data' => []]; // Empty data to trigger error
// Simulate authenticated user (if needed)
if (!defined('ALX_TEST_USER')) define('ALX_TEST_USER', 'Tester');
$controller = new PersonController();
try {
// 2. Execute Protected Method via Reflection
// (Necessary because saveRecord is protected in ResourceController)
$reflection = new \ReflectionClass($controller);
// Initialize internal controller configuration
$configMethod = $reflection->getMethod('buildConfiguration');
$configMethod->setAccessible(true);
$configMethod->invoke($controller);
// Invoke save
$method = $reflection->getMethod('saveRecord');
$method->setAccessible(true);
$method->invoke($controller);
// If no exception happens, the test fails
$this->fail("HttpResponseException was expected due to validation");
} catch (HttpResponseException $e) {
// 3. Verify Error Response
$response = $e->getResponse();
$this->assertArrayHasKey('error', $response);
$this->assertEquals('No data provided', $response['error']);
}
}
/**
* Success test: Correct save.
*/
public function testItCanSaveAPersonViaController()
{
// 1. Valid Data
$_POST = ['data' => [
'name' => 'Jane',
'lastname' => 'Smith',
'active' => 1,
'birth_date' => '1995-05-05'
]];
$controller = new PersonController();
$controller->recordId = 'new'; // Simulate creation
try {
// ... (Reflection setup similar to above) ...
$reflection = new \ReflectionClass($controller);
$configMethod = $reflection->getMethod('buildConfiguration');
$configMethod->setAccessible(true);
$configMethod->invoke($controller);
$method = $reflection->getMethod('saveRecord');
$method->setAccessible(true);
$method->invoke($controller);
$this->fail("JSON success response was expected");
} catch (HttpResponseException $e) {
// 2. Verify Success Response
$response = $e->getResponse();
$this->assertArrayHasKey('status', $response);
$this->assertEquals('success', $response['status']);
$this->assertArrayHasKey('id', $response);
// 3. Verify Persistence in DB
$this->assertDatabaseHas('people', [
'id' => $response['id'],
'name' => 'Jane'
]);
}
}
}Executing Tests
Tests must always be executed from within the Docker container to ensure the environment (PHP, extensions, database) is correct.
Main Commands
Execute all tests (Unit and Feature):
docker exec alxarafe_php ./vendor/bin/phpunitExecute only a specific suite:
docker exec alxarafe_php ./vendor/bin/phpunit --testsuite Unit
docker exec alxarafe_php ./vendor/bin/phpunit --testsuite FeatureExecute a specific test file:
docker exec alxarafe_php ./vendor/bin/phpunit skeleton/Tests/Unit/PersonTest.phpStyle Verification (PSR-12)
It is critical to maintain code style in tests. Alxarafe enforces PSR-12. Test method names must be camelCase (e.g., testItDoSomething), not snake_case.
To verify style:
docker exec alxarafe_php ./vendor/bin/phpcs --standard=PSR12 Tests/ skeleton/Tests/If there are automatically correctable errors:
docker exec alxarafe_php ./vendor/bin/phpcbf --standard=PSR12 Tests/ skeleton/Tests/