Quick reference for running the test suites. For full setup documentation — bootstrap wiring, suite configuration, writing new tests — see Testing with Codeception.
Suite selection: Use Functional for API endpoint testing (emulates HTTP through the Symfony kernel, no browser needed). Browser end-to-end testing uses Playwright (tests/e2e, yarn test:e2e).
Before running any tests make sure:
composer install and yarn installAPP_ENV=test php bin/console doctrine:schema:createconfig/constants.php exists and is valid# Run all tests
php bin/phpunit
# Run a specific directory
php bin/phpunit tests/System/Core/Cache/
php bin/phpunit tests/System/Classes/
php bin/phpunit tests/Functions/
# Run a specific file
php bin/phpunit tests/System/Core/Cache/RedisCacheAdapterTest.php
# Run a specific test method
php bin/phpunit --filter testDeleteByKeyPattern tests/System/Core/Cache/RedisCacheAdapterTest.php
# Run with coverage (requires Xdebug or PCOV)
php bin/phpunit --coverage-html var/coverage
# Run with verbose output
php bin/phpunit --verbose
# Stop on first failure
php bin/phpunit --stop-on-failure
Copy phpunit.xml.dist to phpunit.xml to customise your local run without affecting the committed configuration:
cp phpunit.xml.dist phpunit.xml
PHPUnit test classes can sit anywhere in the tests/ directory. phpunit.xml.dist configures the run:
<phpunit bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">App</directory>
</include>
</coverage>
</phpunit>
The cache adapter tests are good examples of framework-aware PHPUnit tests:
// tests/System/Core/Cache/RedisCacheAdapterTest.php
namespace PHP_SF\Tests\System\Core\Cache;
use PHP_SF\System\Core\Cache\RedisCacheAdapter;
use PHPUnit\Framework\TestCase;
final class RedisCacheAdapterTest extends TestCase
{
protected function tearDown(): void
{
// Clean up after each test
rca()->clear();
}
public function testGetInstance(): void
{
$instance1 = RedisCacheAdapter::getInstance();
$instance2 = RedisCacheAdapter::getInstance();
$this->assertInstanceOf( RedisCacheAdapter::class, $instance1 );
$this->assertSame( $instance1, $instance2 );
}
public function testSetAndGet(): void
{
rca()->set( 'test_key', 'test_value' );
$this->assertSame( 'test_value', rca()->get( 'test_key' ) );
}
public function testDelete(): void
{
rca()->set( 'test_key', 'test_value' );
rca()->delete( 'test_key' );
$this->assertNull( rca()->get( 'test_key' ) );
}
}
Because tests/bootstrap.php boots the full framework kernel, tests run against real Redis, real database connections, and real cache adapters — there's no mocking infrastructure built into the framework. Use tearDown() to clean up after each test.
# Run all suites
php vendor/bin/codecept run
# Run a specific suite
php vendor/bin/codecept run Unit
php vendor/bin/codecept run Functional
# Run a specific test file
php vendor/bin/codecept run Unit ExampleUnitCest
# Run a specific test method
php vendor/bin/codecept run Unit ExampleUnitCest:testSomething
# Run with coverage
php vendor/bin/codecept run --coverage
php vendor/bin/codecept run --coverage-html
# Run only previously failed tests
php vendor/bin/codecept run -g failed
# Run with steps output
php vendor/bin/codecept run --steps
# Run with verbose debug output
php vendor/bin/codecept run --debug
The RunFailed Codeception extension tags failed tests automatically — after fixing a failure, re-run only failed tests with -g failed rather than the full suite.
# Run all Jest tests
yarn test:unit
# Watch mode — re-runs on file change
yarn test:unit --watch
# Run a specific test file
yarn test:unit src/Tests/Unit/ApiTokenHelper.test.ts
# With coverage
yarn test:unit --coverage
Configured in jest.config.js:
// jest.config.js
module.exports = {
testEnvironment: 'node',
roots: [ '<rootDir>/src/Tests/Unit' ],
testMatch: [
'**/?(*.)+(spec|test).[tj]s?(x)'
],
};
Jest tests live in src/Tests/Unit/ and test pure frontend logic — value objects, enums with logic, formatters, cache adapters, helpers. They don't have access to PHP or the framework. DOM-bound code belongs in Playwright e2e (tests/e2e).
k6 must be installed separately — it is not in the npm or Composer dependency tree. See k6.io for installation instructions.
The application must be running before executing performance tests:
./run.sh
# Basic run — single virtual user
k6 run src/Tests/Performance/load.test.js
# With virtual users and duration
k6 run --vus 10 --duration 30s src/Tests/Performance/load.test.js
# Ramp up, sustain, ramp down
k6 run --stage 10s:10,30s:50,10s:0 src/Tests/Performance/load.test.js
# With summary output to file
k6 run --out json=results.json src/Tests/Performance/load.test.js
Test files live in src/Tests/Performance/:
// src/Tests/Performance/load.test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
let res = http.get( 'https://127.0.0.1:7000' );
check( res, {
'status was 200': ( r ) => r.status === 200
} );
sleep( 1 );
}
Use performance tests to:
The framework ships with tests for its own internal components. These run as part of the project test suite via PHPUnit.
Tests for all three cache adapters covering the full PSR-16 interface — get, set, delete, has, getMultiple, setMultiple, deleteMultiple, deleteByKeyPattern, and clear:
tests/System/Core/Cache/
├── RedisCacheAdapterTest.php
├── APCuCacheAdapterTest.php
└── MemcachedCacheAdapterTest.php
APCu tests are skipped automatically when APCu is not available or not enabled:
protected function setUp(): void
{
if ( $this->isAPCuEnabled === false )
$this->markTestSkipped( 'APCu is not enabled' );
}
Each test cleans up after itself via tearDown() — aca()->clear(), rca()->clear(), mca()->clear() — so tests don't bleed state between methods.
Tests for all three middleware composition types — MiddlewareAll, MiddlewareAny, MiddlewareCustom — covering:
tests/System/Classes/MiddlewareChecks/
└── MiddlewaresExecutorTest.php
These tests require the kernel instance from $GLOBALS['kernel'] set in bootstrap.php and use four local middleware stubs defined at the top of the file:
final class MiddlewareTrue extends Middleware
{ public function result(): bool { return true; } }
final class MiddlewareFalse extends Middleware
{ public function result(): bool { return false; } }
final class MiddlewareJsonResponse extends Middleware
{ public function result(): JsonResponse { return new JsonResponse( [ 'test' => 'test' ] ); } }
final class MiddlewareRedirectResponse extends Middleware
{ public function result(): RedirectResponse { return $this->redirectTo( 'welcome_page' ); } }
Router $currentRoute must be set manually for tests that check redirect vs JSON response behaviour since the middleware checks the current URL to decide which response type to return:
// API route — middleware blocks return JsonResponse
Router::$currentRoute = (object)[ 'url' => '/api/middleware_testing' ];
// Page route — middleware blocks return RedirectResponse
Router::$currentRoute = (object)[ 'url' => '/middleware_testing' ];
Tests for the input() view helper function covering every parameter — id, required, placeholder, default value, type, number step, minMax, classes, styles, custom attributes — including all validation exceptions:
tests/Functions/
└── InputFunctionTest.php
These are pure unit tests with no external dependencies — no Redis, no database, no kernel needed. They test string output directly:
public function testId(): void
{
$actual = input( 'input_name', id: 'number' );
$expected = "<input type='text' required id='number' name='input_name' minlength='1' maxlength='255'>";
$this->assertEquals( $expected, $actual );
}
php bin/phpunit tests/System/Core/Cache/
php bin/phpunit tests/System/Classes/MiddlewareChecks/
php bin/phpunit tests/Functions/
php bin/phpunit tests/System/ tests/Functions/
php bin/phpunit tests/Unit/ tests/Functional/
A typical CI sequence that covers the full test stack:
# 1. Install dependencies
composer install --no-interaction
yarn install
# 2. Set up test environment
cp .env.test .env.local
APP_ENV=test php bin/console doctrine:schema:create
APP_ENV=test php bin/console doctrine:fixtures:custom-loader -f
# 3. Clear all cache
php bin/console app:cache:clear
php bin/console symfony:cache:clear
# 4. Run PHPUnit
php bin/phpunit --stop-on-failure
# 5. Run Codeception unit and functional suites
php vendor/bin/codecept run Unit
php vendor/bin/codecept run Functional
# 6. Run Jest
yarn test:unit
# 7. (Optional) Run Playwright e2e tests
yarn test:e2e
Playwright e2e tests are typically separated from the main CI pipeline or run on a schedule rather than on every commit — they're slow and require a running application.
Running tests without Redis — RedisCacheAdapterTest and any test that uses ca(), rca(), or em() requires Redis to be running. A missing Redis connection fails immediately with a connection refused error. Start Docker services before running tests:
docker-compose up -d redis postgres
Running Playwright e2e tests without the dev server — Playwright tests (tests/e2e) require the application to be running. Start the dev server before running yarn test:e2e.
Not copying phpunit.xml.dist — phpunit.xml is gitignored. On a fresh clone, php bin/phpunit uses phpunit.xml.dist. If you customise your local run (different filter, different coverage output path), copy it to phpunit.xml first so your changes don't affect other developers.
Skipping tearDown() in new tests — tests that write to Redis or the database without cleaning up in tearDown() cause unpredictable failures in subsequent test methods. The order that PHPUnit runs test methods within a class is not guaranteed. Always clean up.