The template ships with Codeception pre-configured as the test runner for your application. It is not tied to the framework in any way — if you prefer PHPUnit, Pest, or no test framework at all, you can remove it.
This page covers Codeception setup and writing tests. For run commands across the whole stack — Codeception, PHPUnit, Jest, k6, Playwright — see Running Tests.
Suite selection at a glance:
Browser end-to-end testing uses Playwright (tests/e2e, yarn test:e2e) — see Running Tests.
tests/
├── Functional/
│ ├── ExampleFunctionalCest.php
│ └── .gitignore
├── Unit/
│ └── ExampleUnitCest.php
├── Support/
│ ├── FunctionalTester.php
│ ├── UnitTester.php
│ ├── Uni2Tester.php
│ ├── Helper/
│ │ ├── Functional.php
│ │ └── Unit.php
│ └── _generated/ ← auto-generated, not committed
├── e2e/ ← Playwright browser tests
├── _data/ ← test fixtures data
├── _output/ ← test output, not committed
├── Functional.suite.yml
├── Unit.suite.yml
└── bootstrap.php
Jest (src/Tests/Unit/) and k6 (src/Tests/Performance/) tests live outside tests/ — see Running Tests.
The test bootstrap wires the framework kernel the same way public/index.php does, with a few additions for the test context:
// tests/bootstrap.php
require_once __DIR__ . '/../vendor/autoload.php';
// PHPUnit sets APP_ENV=test via phpunit.xml.dist <server> before this runs.
// Codeception does not, so we default it here so bootEnv('.env') picks up .env.test.
$_SERVER['APP_ENV'] ??= 'test';
$_ENV['APP_ENV'] ??= 'test';
( new Dotenv() )->bootEnv( __DIR__ . '/../.env' );
$kernel = ( new PHP_SF\Kernel() )
->addTranslationFiles( __DIR__ . '/../translations' )
->addControllers( __DIR__ . '/../App/Http/Controller' )
->setHeaderTemplateClassName( header::class )
->setFooterTemplateClassName( footer::class )
->setApplicationUserClassName( User::class )
->addTemplatesDirectory( 'templates', 'App\View' );
Router::loadRoutesOnly( $kernel );
auth::logInUser();
$GLOBALS['kernel'] = $kernel;
Router::loadRoutesOnly( $kernel ) pre-populates the framework's route list from the registered controller directories without dispatching any request. This is required so that PhpSfRouteLoader (used by Codeception's Symfony module) can register all PHP_SF routes into Symfony's compiled router cache.
$GLOBALS['kernel'] stores the kernel instance for test classes that need direct access — the MiddlewaresExecutorTest uses it to construct middleware with the correct kernel instance.
auth::logInUser() with no arguments attempts to restore an authenticated user from the session. In the test environment this typically finds nothing and sets auth::$user = false, but it's called to match the production boot sequence.
codeception.yml at the project root configures the runner:
namespace: Tests
support_namespace: Support
paths:
tests: tests
output: tests/_output
data: tests/_data
support: tests/Support
actor_suffix: Tester
extensions:
enabled:
- Codeception\Extension\RunFailed
coverage:
enabled: true
include:
- App/*
- Platform/*
- templates/*
- functions/*
low_limit: 30
high_limit: 60
show_uncovered: true
env:
.env.test
Coverage includes App/, Platform/, templates/, and functions/. Low watermark is 30%, high is 60% — below 30% the build fails, above 60% coverage is considered acceptable. Adjust these thresholds as the test suite matures.
.env.test is loaded for all test runs — keep test-specific environment overrides there (test database URL, etc.).
# tests/Unit.suite.yml
actor: Uni2Tester
suite_namespace: Tests\Uni2
modules:
enabled:
- Asserts
- \Tests\Support\Helper\Unit
bootstrap: ../bootstrap.php
Unit tests use Uni2Tester (note the naming — a second unit tester actor, the first UnitTester exists for compatibility). The Asserts module provides assertion methods directly on the tester.
Unit test files use the Cest suffix:
// tests/Unit/ExampleUnitCest.php
namespace Tests\Unit;
use PHP_SF\System\Core\Cache\RedisCacheAdapter;
use Tests\Support\Uni2Tester;
class ExampleUnitCest
{
public function testSomething( Uni2Tester $I ): void
{
$I->assertInstanceOf( RedisCacheAdapter::class, ca() );
}
}
Since the framework kernel is booted in bootstrap.php, all framework helpers are available in unit tests — ca(), em(), rca(), entity static finders, everything. This makes it straightforward to test against real Redis and real database connections rather than mocking everything.
# tests/Functional.suite.yml
actor: FunctionalTester
modules:
enabled:
- Symfony:
app_path: 'App'
environment: 'test'
- Asserts
- \Tests\Support\Helper\Functional
bootstrap: ../bootstrap.php
Functional tests use Symfony's Codeception module — they emulate HTTP requests through the Symfony kernel without a real browser. This is the right suite for testing API endpoints: it's fast, has full access to the Symfony container, and supports JSON response assertions without needing a running server. Both Symfony controllers and PHP_SF framework controllers are supported:
// tests/Functional/ExampleFunctionalCest.php
namespace Tests\Functional;
use Tests\Support\FunctionalTester;
class ExampleFunctionalCest
{
// Tests a native Symfony controller (App/Http/SymfonyControllers/)
public function testSymfonyControllerReturnsJson( FunctionalTester $I ): void
{
$I->amOnPage( '/example/symfony' );
$I->seeResponseCodeIs( 200 );
$I->seeInSource( '{"key":"value"}' );
}
// Tests a PHP_SF framework controller (App/Http/Controller/)
public function testPhpSfFrameworkControllerReturnsJSON( FunctionalTester $I ): void
{
$I->amOnPage( '/example/page/json_response' );
$I->seeResponseCodeIs( 200 );
$I->seeInSource( '{"status":"ok"}' );
}
}
PHP_SF framework routes are available in functional tests because PHP_SF\Framework\Routing\PhpSfRouteLoader is registered as a Symfony routing.loader service (active only in the test environment via when@test in config/routes.yaml). This loader runs during Symfony's cache warming and includes all PHP_SF routes in the compiled router matcher, so Codeception's kernel reboots between tests do not lose the routes.
For run commands — all suites, single suite/file/method, coverage, re-running failures — see Running Tests. The RunFailed extension (enabled in codeception.yml) tags failed tests with the failed group, so you can re-run only failures with php vendor/bin/codecept run -g failed.
.env.test holds test-specific environment overrides:
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=disabled
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
For a separate test database, add an override to .env.test or set it per connection:
DATABASE_MAIN_DBNAME_TEST=nations-original-app_test
The test database name is configured in config/packages/doctrine.yaml. Each connection has an explicit when@test override that uses the default: env processor to fall back to the base name with a _test suffix if no explicit test name is provided:
when@test:
doctrine:
dbal:
connections:
main:
dbname: '%env(default:app.db_main_test_dbname:DATABASE_MAIN_DBNAME_TEST)%'
parameters:
app.db_main_test_dbname: '%env(DATABASE_MAIN_DBNAME)%_test'
Create the test database schema (database must be created before schema):
bin/console doctrine:database:create --if-not-exists --em=main --env=test
bin/console doctrine:schema:create --em=main --env=test
Codeception generates coverage reports, configured to include App/, Platform/, templates/, and functions/:
# Codeception HTML coverage report
php vendor/bin/codecept run --coverage --coverage-html
Reports are written to tests/_output/ (gitignored). PHPUnit coverage commands are in Running Tests.
The 30%/60% thresholds in codeception.yml are starting points. For a game backend with complex business logic, I aim higher — 70%+ coverage on entity validation, middleware, and core game mechanics significantly reduces regression risk.
Not cleaning up after tests — framework tests run against real Redis and real database connections. Without tearDown() cleanup, test data bleeds between test methods and causes unpredictable failures depending on test execution order. Always clean up in tearDown().
Running Playwright e2e tests without the app running — Playwright tests (tests/e2e) require the application to be running. Start the dev server before running yarn test:e2e.
Forgetting to create the test database — if DATABASE_URL in .env.test points to a separate test database, it must be created and migrated before tests run. A missing test database causes all database-touching tests to fail with connection errors.
Using DEV_MODE = true in test environment — APCu is cleared on every request boot in DEV_MODE, which adds overhead to every test. Set DEV_MODE = false in config/constants.php when running the test suite for accurate performance and to test production cache behaviour. Or maintain a separate config/constants.test.php and load it in bootstrap.php.