DÉMO
dev only

Testing

PHPUnit test suite — 72 tests across Unit, Integration and E2E layers.

Overview

The test suite covers three layers: Unit (pure logic), Integration (services + real database) and E2E (HTTP client + full stack). All tests run inside the Docker container against a dedicated test database.

Suite Tests Description
Unit 21 Pure PHP logic, no I/O (Services, Voters, Registries…)
Integration 31 Services against a real PostgreSQL database via KernelTestCase
Functional 20 HTTP client via WebTestCase — controllers, forms, redirects

First-time setup

The integration and E2E suites require a dedicated test database. Create it once, then run tests at will:

make test-db

Running the tests

Use Make targets from your host machine:

Command Description
make test Full test suite (Unit + Integration + E2E)
make test-unit Unit tests only
make test-integration Integration tests only (requires test-db)
make test-functional E2E / functional tests only (requires test-db)

Foundry factories

All entities have a Zenstruck Foundry factory under src/Factory/. Each factory ships default values and state methods for common scenarios:

Factory States
src/Factory/UserFactory.php
src/Factory/OrganizationFactory.php
src/Factory/OrganizationMemberFactory.php owner() suspended()
src/Factory/InvitationFactory.php expired() accepted()
src/Factory/SubscriptionFactory.php trialing() active() pastDue() expired()
src/Factory/PasswordResetTokenFactory.php
src/Factory/ScheduledTaskFactory.php lastRunSuccess() lastRunFailure() disabled()
src/Factory/ScheduledTaskExecutionFactory.php

AppStory

AppStory populates the development database with realistic personas — two organisations, multiple members per role, subscription states and scheduled tasks. Run it with:

make seed

Test isolation

Stripe API calls are replaced by a no-op stub in the test environment. The stub is wired via config/services_test.yaml (loaded after config/services.yaml so it takes precedence):

File Role
tests/Stub/StripeServiceStub.php No-op StripeService: returns fake IDs, never calls the Stripe API
config/services_test.yaml Replaces StripeService with the stub in the test container
config/packages/test/framework.yaml framework.test=true, disables form CSRF, sets rate limiters to no_limit

Key patterns

A few conventions that apply across all test classes:

  1. 1

    In E2E tests (WebTestCase), always call createClient() before any factory call — factories boot the kernel, and calling createClient() a second time throws a LogicException.

    $client = static::createClient(); // FIRST $user = UserFactory::createOne(); // THEN factories
  2. 2

    Always use Factory::new()->state() — state() is an instance method in Foundry v2 and PHP 8.4 does not allow static calls to non-static methods.

    // Correct (Foundry v2 + PHP 8.4) SubscriptionFactory::new()->trialing()->create(); // Wrong — PHP 8.4 fatal error SubscriptionFactory::trialing()->create();
  3. 3

    All rate limiters use policy: no_limit in the test environment (config/packages/test/framework.yaml) so WebTestCase requests are never blocked.

    config/packages/test/framework.yaml
  4. 4

    Form CSRF is disabled in tests (form.csrf_protection: false). Login and logout forms use the Twig csrf_token() function directly and are not affected.

Loading…
Loading the web debug toolbar…
Attempt #