Testing & QA
MonkeysLegion ships with no test framework baked in, so you’re free to use PHPUnit, Pest, or any runner you like—but the packages are designed to be easy to bootstrap, mock, and assert against.
0 · TL;DR
composer require --dev phpunit/phpunit vimeo/psalm squizlabs/php_codesniffer
vendor/bin/phpunit # run unit + integration
vendor/bin/psalm --shepherd # static analysis
vendor/bin/phpcs --standard=PSR12 app/ # code style
Add a GitHub Action to run those three commands and you already catch >90 % of issues before merge.
1 · Directory layout
tests/
├─ Unit/
│ └─ Services/...
├─ Feature/
│ ├─ Http/
│ └─ Console/
└─ Playwright/ # optional UI tests
phpunit.xml.dist
Set bootstrap="tests/bootstrap.php" in phpunit.xml and build the container there:
// tests/bootstrap.php
$container = (new MonkeysLegion\DI\ContainerBuilder())
->addDefinitions(require __DIR__ . '/../config/services.php')
->build();
$GLOBALS['ml_container'] = $container; // handy for helpers
2 · Unit Tests
namespace Tests\Unit\Services;
use PHPUnit\Framework\TestCase;
use App\Service\Slugger;
class SluggerTest extends TestCase
{
public function test_it_slugs_utf8(): void
{
$slug = (new Slugger())->slugify('Héllö World!');
$this->assertSame('hello-world', $slug);
}
}
Keep unit tests pure PHP—no DB, no filesystem. Mock collaborators with Prophecy or Mockery.
3 · Feature / HTTP tests
namespace Tests\Feature\Http;
use PHPUnit\Framework\TestCase;
use Laminas\Diactoros\ServerRequestFactory;
class PostApiTest extends TestCase
{
private $app;
protected function setUp(): void
{
// bootstrap HTTP kernel once
$this->app = $GLOBALS['ml_container']
->get(MonkeysLegion\Http\MiddlewareDispatcher::class);
}
public function test_list_posts_returns_empty_array(): void
{
$req = ServerRequestFactory::fromGlobals()->withMethod('GET')
->withUri(new \Laminas\Diactoros\Uri('/posts'));
$res = $this->app->handle($req);
$this->assertSame(200, $res->getStatusCode());
$this->assertJsonStringEqualsJsonString('[]', (string) $res->getBody());
}
}
Spin up no real server—just dispatch the PSR-7 request against the in-memory middleware pipeline.
4 · Database tests
Point .env.test (or config/env/test.mlc) at SQLite :memory:
In tests/bootstrap.php run CLI migrations:
(new MonkeysLegion\Migration\MigrateCommand(
$GLOBALS['ml_container']->get(MonkeysLegion\Database\Connection::class)
))->handle();
Wrap each test in a transaction:
protected function setUp(): void
{
$this->db = $GLOBALS['ml_container']->get(Connection::class);
$this->db->pdo()->beginTransaction();
}
protected function tearDown(): void
{
$this->db->pdo()->rollBack();
}
Fast, isolated, no residual state.
5 · Validation & Auth scenarios
public function test_registration_fails_with_short_password(): void
{
$payload = ['email'=>'a@b.c', 'password'=>'123'];
$req = (new ServerRequestFactory)->createServerRequest('POST','/auth/register')
->withParsedBody($payload);
$res = $this->app->handle($req);
$this->assertSame(422, $res->getStatusCode());
$body = json_decode((string) $res->getBody(), true);
$this->assertSame('Length', $body['errors'][0]['rule']);
}
ValidationMiddleware automatically returns 422 JSON on rule violations—assert against that.
6 · Front-end / Visual tests
npm i -D @playwright/test
Create tests/Playwright/visual.spec.ts:
test('home page loads', async ({ page }) => {
await page.goto(process.env.APP_URL ?? 'http://localhost:8000');
await expect(page).toHaveTitle(/MonkeysLegion/);
expect(await page.screenshot()).toMatchSnapshot('home.png');
});
Run Playwright in CI after PHP tests; fail on diff.
7 · Static analysis & style
Psalm or PHPStan at level 6+
PHP_CodeSniffer with PSR-12 or your own ruleset
Deptrac (optional) to keep domain layers independent
Add a Composer script:
"scripts": {
"analyse": "psalm --shepherd && phpcs --standard=PSR12 app/",
"lint": "php-cs-fixer fix --dry-run"
}
8 · Quality gates in CI (GitHub Actions)
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: myapp_test
ports: [3306:3306]
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: 8.4, extensions: pdo_mysql }
- run: composer install --no-progress --no-interaction
- run: vendor/bin/phpunit --coverage-text
- run: composer analyse
Fail the build if tests, static analysis, or style report errors.
9 · Test helpers & traits (optional)
DatabaseTransactions trait — starts & rolls back a DB transaction per test.
MakesRequests — tiny helper to build PSR-7 requests with JSON / form bodies.
WithFaker — seeds Faker v2 for factories.
Add them in tests/Support/ and reuse across the suite.
10 · Recommended coverage
Layer | Coverage target |
---|---|
Domain services | 90 % lines / 70 % branches |
Repositories | “happy path” + constraint errors |
Controllers | 1 path per route |
Views | visual diff or Playwright snapshots |
CLI commands | 1 success + 1 failure case |
Recap
PHPUnit/Pest for code, Playwright for pixels, Psalm for types, phpcs for style.
Use DI container + in-memory middleware to test HTTP without spinning up a server.
Transaction-wrap DB tests for speed & isolation.
Enforce everything in CI so master/main never breaks.
Happy shipping — with confidence! 🐒✅