From 3f76fce180b09191ca04530b8b8993d1ab75a098 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 7 May 2026 10:30:59 +0300 Subject: [PATCH 1/4] Add testing guide --- src/guide/index.md | 12 ++-- src/guide/testing/end-to-end.md | 29 ++++++++++ src/guide/testing/environment-setup.md | 36 ++++++++++++ src/guide/testing/functional.md | 77 ++++++++++++++++++++++++++ src/guide/testing/overview.md | 49 ++++++++++++++++ src/guide/testing/quality-tools.md | 23 ++++++++ src/guide/testing/unit.md | 38 +++++++++++++ 7 files changed, 258 insertions(+), 6 deletions(-) create mode 100644 src/guide/testing/end-to-end.md create mode 100644 src/guide/testing/environment-setup.md create mode 100644 src/guide/testing/functional.md create mode 100644 src/guide/testing/overview.md create mode 100644 src/guide/testing/quality-tools.md create mode 100644 src/guide/testing/unit.md diff --git a/src/guide/index.md b/src/guide/index.md index 647581e4..9fb688f2 100644 --- a/src/guide/index.md +++ b/src/guide/index.md @@ -117,12 +117,12 @@ We release this guide under the [Terms of Yii Documentation](https://www.yiifram ## Testing -- [Testing overview](testing/overview.md) TODO -- [Testing environment setup](testing/environment-setup.md) TODO -- [Unit tests](testing/unit.md) TODO -- [Functional tests](testing/functional.md) TODO -- [Acceptance tests](testing/acceptance.md) TODO -- [Fixtures](testing/fixtures.md) TODO +- [Testing overview](testing/overview.md) +- [Testing environment setup](testing/environment-setup.md) +- [Unit tests](testing/unit.md) +- [Functional tests](testing/functional.md) +- [End-to-end tests](testing/end-to-end.md) +- [Static analysis and mutation testing](testing/quality-tools.md) ## Special topics diff --git a/src/guide/testing/end-to-end.md b/src/guide/testing/end-to-end.md new file mode 100644 index 00000000..a4159857 --- /dev/null +++ b/src/guide/testing/end-to-end.md @@ -0,0 +1,29 @@ +# End-to-end tests + +End-to-end tests run the application through a real client. The client can be a browser, command-line HTTP client, or +another system that talks to the application over HTTP. + +Use end-to-end tests for user-visible flows: + +- Sign in and sign out. +- Form submission and validation messages. +- JavaScript behavior. +- File uploads. +- Integration with reverse proxies, headers, cookies, and redirects. + +These tests are slower and more sensitive to infrastructure than unit and functional tests. Keep them focused on +critical flows and cover detailed business rules with lower-level tests. + +## Environment + +Run end-to-end tests against a dedicated test environment. The web server, PHP process, database, cache, and external +service fakes should match the way the application runs in development or CI. + +Prepare state before each scenario: + +- Load only the records required by the scenario. +- Clear session and cookie storage. +- Clear generated files and outgoing messages. +- Stop background workers or make their effects deterministic. + +After the test, assert on user-visible output and durable effects such as database rows or generated files. diff --git a/src/guide/testing/environment-setup.md b/src/guide/testing/environment-setup.md new file mode 100644 index 00000000..edf44aaf --- /dev/null +++ b/src/guide/testing/environment-setup.md @@ -0,0 +1,36 @@ +# Testing environment setup + +Use a dedicated test environment. It should have its own configuration, runtime directory, and database connection. +This keeps tests repeatable and protects development data. + +A typical setup includes: + +- `APP_ENV=test` or an equivalent environment flag. +- A database created only for tests. +- Separate cache, session, and queue storage. +- Local implementations for external services such as mailers, payment providers, and HTTP APIs. +- A test bootstrap that loads Composer autoloading and prepares the application configuration. + +## Reset state + +Reset application state before each test or test group. The exact reset strategy depends on the storage: + +- Recreate the schema, truncate tables, or wrap tests in transactions for database state. +- Clear cache pools, runtime directories, and generated files. +- Reset session and cookie storage. +- Empty queues and captured outgoing messages. +- Restore fake clocks, random generators, and global configuration overrides. + +Use the same reset rules for local runs and CI. A test suite should pass when run from a clean checkout and when run +after another test suite. + +## Run tests locally + +Use the command configured by the project. For a project that uses PHPUnit directly, the command can look like this: + +```shell +APP_ENV=test vendor/bin/phpunit +``` + +When the application runs in Docker, run the test command inside the test container and point it to the test +environment files. Keep host services, container services, and CI services configured the same way where possible. diff --git a/src/guide/testing/functional.md b/src/guide/testing/functional.md new file mode 100644 index 00000000..34c7248a --- /dev/null +++ b/src/guide/testing/functional.md @@ -0,0 +1,77 @@ +# Functional tests + +Functional tests check how application parts work together in the same PHP process. For web applications, use Yii's +PSR-7 request-response flow: create a PSR-7 server request, pass it to the application or middleware stack, and assert +on the PSR-7 response. + +This style covers routing, middleware, action handlers, container configuration, view rendering, and response headers +without starting a web server. + +## Request and response + +Create the request with a PSR-17 server request factory from the PSR-7 implementation used by the project: + +```php +application, $this->requestFactory] = $runtime; + } + + public function testHomePageReturnsSuccessfulResponse(): void + { + $request = $this->requestFactory->createServerRequest('GET', '/'); + + $response = $this->handle($request); + + self::assertSame(200, $response->getStatusCode()); + self::assertStringContainsString('Welcome', (string) $response->getBody()); + } + + private function handle(ServerRequestInterface $request): ResponseInterface + { + $this->application->start(); + + try { + $response = $this->application->handle($request); + } finally { + $this->application->shutdown(); + } + + $body = $response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + + return $response; + } +} +``` + +The `bootstrap-web-test.php` file is project-specific. It should build the test container and return the +`Yiisoft\Yii\Http\Application` instance together with the PSR-17 server request factory used by the project. + +## Reset state + +Functional tests usually touch more state than unit tests. Reset the database, cache, sessions, files, and outgoing +messages before the next request. If a test sends multiple requests, reset state only between scenarios that must be +independent. + +Use functional tests for behavior that needs application wiring. Keep pure domain rules in unit tests. diff --git a/src/guide/testing/overview.md b/src/guide/testing/overview.md new file mode 100644 index 00000000..b07a5264 --- /dev/null +++ b/src/guide/testing/overview.md @@ -0,0 +1,49 @@ +# Testing + +Tests help keep Yii applications and packages safe to change. Use the smallest test type that can prove the behavior: + +1. Unit tests. +2. Functional tests. +3. End-to-end tests. + +This order keeps feedback fast. Unit tests are the cheapest to write and run. Functional tests cover application +integration through a real request and response. End-to-end tests exercise the application through an HTTP server or +browser and are best reserved for the flows that must work from the user's point of view. + +## Test types + +Unit tests check a small unit of code in isolation: a value object, service, middleware, handler, validator, or domain +operation. They should avoid bootstrapping the whole application when a direct object call is enough. + +Functional tests run application code in the same PHP process. For web functionality, create a PSR-7 +`ServerRequestInterface`, pass it to the application or middleware stack, and assert on the returned PSR-7 +`ResponseInterface`. This tests routing, middleware, container configuration, and response creation without starting a +web server. + +End-to-end tests use the application the same way a user or external system uses it: through HTTP requests, a browser, +or another real client. They are useful for forms, authentication flows, JavaScript behavior, and integration with +services that are hard to represent in-process. + +## Application state + +Tests should start from a known application state and leave the application ready for the next test. Reset the same +state that a real application changes: + +- Database rows. +- Cache entries. +- Sessions and cookies. +- Uploaded or generated files. +- Queues and outgoing messages. +- Time, random values, and other process-wide fakes. + +Prefer a separate test environment, separate test database, and isolated runtime directories. A test that depends on +leftover data from a previous test can pass locally and fail in CI. + +## Quality gates + +Automated tests are only one part of the feedback loop. Static analysis checks type contracts and unreachable or unsafe +code paths before the code runs. Mutation testing changes small parts of the source code and checks whether the test +suite fails; surviving mutations point to assertions that are missing or too weak. + +Run unit and functional tests frequently during development. Run end-to-end tests, static analysis, and mutation testing +in CI or before releases, based on the project's size and cost of failure. diff --git a/src/guide/testing/quality-tools.md b/src/guide/testing/quality-tools.md new file mode 100644 index 00000000..1265b9c0 --- /dev/null +++ b/src/guide/testing/quality-tools.md @@ -0,0 +1,23 @@ +# Static analysis and mutation testing + +Tests execute selected examples. Static analysis and mutation testing add different feedback. + +Static analysis reads the code and checks type contracts, control flow, unreachable code, invalid calls, and other +issues before the code runs. Common PHP tools include [Psalm](https://psalm.dev/) and +[PHPStan](https://phpstan.org/). + +Mutation testing changes small parts of the source code and runs the test suite against each change. If the tests still +pass, the changed code is a surviving mutation. Surviving mutations often mean that assertions are missing, too broad, +or checking implementation details instead of behavior. A common PHP mutation testing tool is +[Infection](https://infection.github.io/). + +## When to run them + +Run static analysis in CI for every pull request. Run it locally before pushing changes that affect shared interfaces, +container configuration, or generated types. + +Mutation testing is more expensive. Run it for packages, domain code, and critical services where test strength matters. +For large applications, start with a small source path and expand the scope as the suite becomes faster and more stable. + +Use these tools with tests. Static analysis finds many type and flow problems. Mutation testing shows whether tests +would catch behavior changes. diff --git a/src/guide/testing/unit.md b/src/guide/testing/unit.md new file mode 100644 index 00000000..926f3e11 --- /dev/null +++ b/src/guide/testing/unit.md @@ -0,0 +1,38 @@ +# Unit tests + +Unit tests check a small piece of code directly. They are best for domain rules, pure services, value objects, data +transformers, middleware branches, validators, and error handling. + +Keep unit tests focused: + +- Instantiate the class under test directly. +- Pass test doubles for dependencies that perform I/O. +- Assert the returned value, changed object state, or thrown exception. +- Cover one behavior per test method. + +Avoid loading the full application for code that can be tested with constructor arguments and method calls. + +## Example + +```php +generate('Hello, Yii 3!'); + + self::assertSame('hello-yii-3', $slug); + } +} +``` + +If a class depends on time, randomness, network, filesystem, or a database, hide that dependency behind an interface and +pass a deterministic implementation in the test. From 8854829f3072e3a30a7c386f1471cbc3d78c82a9 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 7 May 2026 17:39:07 +0300 Subject: [PATCH 2/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/guide/testing/unit.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/guide/testing/unit.md b/src/guide/testing/unit.md index 926f3e11..63c9456e 100644 --- a/src/guide/testing/unit.md +++ b/src/guide/testing/unit.md @@ -27,9 +27,9 @@ final class SlugGeneratorTest extends TestCase { $generator = new SlugGenerator(); - $slug = $generator->generate('Hello, Yii 3!'); + $slug = $generator->generate('Hello, Yii3!'); - self::assertSame('hello-yii-3', $slug); + self::assertSame('hello-yii3', $slug); } } ``` From 58a0403a42cd6da179bb437ad6f6e71ebda2ff7e Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 7 May 2026 18:58:25 +0300 Subject: [PATCH 3/4] Make testing guide practical --- src/guide/testing/end-to-end.md | 96 +++++++++++--- src/guide/testing/environment-setup.md | 172 ++++++++++++++++++++++--- src/guide/testing/functional.md | 143 ++++++++++++++------ src/guide/testing/overview.md | 114 +++++++++++----- src/guide/testing/quality-tools.md | 120 ++++++++++++++--- src/guide/testing/unit.md | 151 ++++++++++++++++++++-- 6 files changed, 663 insertions(+), 133 deletions(-) diff --git a/src/guide/testing/end-to-end.md b/src/guide/testing/end-to-end.md index a4159857..fc68add3 100644 --- a/src/guide/testing/end-to-end.md +++ b/src/guide/testing/end-to-end.md @@ -1,29 +1,95 @@ # End-to-end tests -End-to-end tests run the application through a real client. The client can be a browser, command-line HTTP client, or -another system that talks to the application over HTTP. +End-to-end tests run the application through a real HTTP server. Use them for user-visible flows such as sign in, form +submission, redirects, cookies, JavaScript behavior, and file uploads. -Use end-to-end tests for user-visible flows: +Keep them few. Detailed business rules belong in unit and functional tests. -- Sign in and sign out. -- Form submission and validation messages. -- JavaScript behavior. -- File uploads. -- Integration with reverse proxies, headers, cookies, and redirects. +## Smoke test with curl -These tests are slower and more sensitive to infrastructure than unit and functional tests. Keep them focused on -critical flows and cover detailed business rules with lower-level tests. +Start the application in the test environment: -## Environment +```shell +APP_ENV=test ./yii serve --port=8080 +``` -Run end-to-end tests against a dedicated test environment. The web server, PHP process, database, cache, and external -service fakes should match the way the application runs in development or CI. +In another terminal, check the home page: -Prepare state before each scenario: +```shell +curl -fsS http://127.0.0.1:8080/ > /tmp/home.html +grep -q "Welcome" /tmp/home.html +``` + +This is enough for a simple deployment or reverse-proxy smoke test. + +## Browser tests with Playwright + +Install Playwright: + +```shell +npm install --save-dev @playwright/test +npx playwright install +``` + +Create `playwright.config.ts`: + +```ts +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: 'tests/EndToEnd', + webServer: { + command: 'APP_ENV=test ./yii serve --port=8080', + url: 'http://127.0.0.1:8080/', + reuseExistingServer: !process.env.CI, + }, + use: { + baseURL: 'http://127.0.0.1:8080', + }, +}); +``` + +Create `tests/EndToEnd/home-page.spec.ts`: + +```ts +import { expect, test } from '@playwright/test'; + +test('home page opens', async ({ page }) => { + await page.goto('/'); + + await expect(page).toHaveTitle(/Yii/i); + await expect(page.getByText('Welcome')).toBeVisible(); +}); +``` + +Run it: + +```shell +npx playwright test +``` + +Run it with a visible browser while debugging: + +```shell +npx playwright test --headed --debug +``` + +## Reset state + +End-to-end tests use real infrastructure, so reset state before each scenario: - Load only the records required by the scenario. - Clear session and cookie storage. - Clear generated files and outgoing messages. - Stop background workers or make their effects deterministic. -After the test, assert on user-visible output and durable effects such as database rows or generated files. +If the scenario changes a database, reset it in a Playwright `beforeEach` hook by calling a project-specific script: + +```ts +import { test } from '@playwright/test'; +import { execFileSync } from 'node:child_process'; + +test.beforeEach(() => { + execFileSync('php', ['tests/reset-test-state.php']); +}); +``` diff --git a/src/guide/testing/environment-setup.md b/src/guide/testing/environment-setup.md index edf44aaf..6eef2604 100644 --- a/src/guide/testing/environment-setup.md +++ b/src/guide/testing/environment-setup.md @@ -1,36 +1,168 @@ # Testing environment setup -Use a dedicated test environment. It should have its own configuration, runtime directory, and database connection. -This keeps tests repeatable and protects development data. +Use a dedicated test environment. It should have its own environment variables, runtime directory, database, cache, and +service fakes. -A typical setup includes: +## Configure PHPUnit -- `APP_ENV=test` or an equivalent environment flag. -- A database created only for tests. -- Separate cache, session, and queue storage. -- Local implementations for external services such as mailers, payment providers, and HTTP APIs. -- A test bootstrap that loads Composer autoloading and prepares the application configuration. +Create `phpunit.xml.dist` in the project root: + +```xml + + + + + tests/Unit + + + tests/Functional + + + + + + src + + + + + + + + +``` + +Create `tests/bootstrap.php`: + +```php + [ + 'file' => '@runtime/test/logs/app.log', + ], +]; +``` + +Use the same approach for a test database, queue, mailer, or cache connection. Keep test credentials separate from +development credentials. ## Reset state -Reset application state before each test or test group. The exact reset strategy depends on the storage: +Reset changed state in `setUp()` or in a project-specific base test case: + +```php +resetDatabase(); + $this->resetCache(); + } -- Recreate the schema, truncate tables, or wrap tests in transactions for database state. -- Clear cache pools, runtime directories, and generated files. -- Reset session and cookie storage. -- Empty queues and captured outgoing messages. -- Restore fake clocks, random generators, and global configuration overrides. + private function resetDatabase(): void + { + // Truncate tables, reload fixtures, or start a transaction. + } -Use the same reset rules for local runs and CI. A test suite should pass when run from a clean checkout and when run -after another test suite. + private function resetCache(): void + { + // Clear the cache storage used by the test environment. + } +} +``` + +Choose one database reset strategy and use it consistently: + +- Recreate the schema when tests need full isolation and the schema is small. +- Truncate tables and load fixtures for application-level tests. +- Use transactions for tests that stay on one database connection. -## Run tests locally +Also reset sessions, cookies, uploaded files, queues, outgoing mail, fake clocks, and generated files when a test +changes them. -Use the command configured by the project. For a project that uses PHPUnit directly, the command can look like this: +## Run locally and in Docker + +Run all tests locally: ```shell APP_ENV=test vendor/bin/phpunit ``` -When the application runs in Docker, run the test command inside the test container and point it to the test -environment files. Keep host services, container services, and CI services configured the same way where possible. +Run one suite: + +```shell +APP_ENV=test vendor/bin/phpunit --testsuite Functional +``` + +With Docker: + +```shell +docker compose -f docker/compose.yml -f docker/test/compose.yml run --rm app vendor/bin/phpunit +``` diff --git a/src/guide/testing/functional.md b/src/guide/testing/functional.md index 34c7248a..af99840d 100644 --- a/src/guide/testing/functional.md +++ b/src/guide/testing/functional.md @@ -1,77 +1,138 @@ # Functional tests -Functional tests check how application parts work together in the same PHP process. For web applications, use Yii's -PSR-7 request-response flow: create a PSR-7 server request, pass it to the application or middleware stack, and assert -on the PSR-7 response. +Functional tests run application code in the same PHP process. For a web application, create a PSR-7 request, pass it +to Yii, and assert on the PSR-7 response. -This style covers routing, middleware, action handlers, container configuration, view rendering, and response headers -without starting a web server. +This checks routing, middleware, action handlers, views, container configuration, headers, cookies, and sessions without +starting a web server. -## Request and response +## Add a web test helper -Create the request with a PSR-17 server request factory from the PSR-7 implementation used by the project: +Create `tests/Support/WebApp.php`: ```php createServerRequest($method, $uri); + } - protected function setUp(): void + public static function handle(ServerRequestInterface $request): ResponseInterface { - /** @var array{Application, ServerRequestFactoryInterface} $runtime */ - $runtime = require __DIR__ . '/bootstrap-web-test.php'; + $runner = new HttpApplicationRunner( + rootPath: dirname(__DIR__, 2), + debug: true, + environment: Environment::TEST, + bootstrapGroup: 'bootstrap-web', + eventsGroup: 'events-web', + diGroup: 'di-web', + diProvidersGroup: 'di-providers-web', + diDelegatesGroup: 'di-delegates-web', + diTagsGroup: 'di-tags-web', + paramsGroup: 'params-web', + nestedParamsGroups: ['params'], + nestedEventsGroups: ['events'], + ); + + $response = $runner->runAndGetResponse($request); + $body = $response->getBody(); + + if ($body->isSeekable()) { + $body->rewind(); + } - [$this->application, $this->requestFactory] = $runtime; + return $response; } +} +``` - public function testHomePageReturnsSuccessfulResponse(): void +If your project uses different configuration group names, take them from `config/configuration.php`. + +## Test a page + +Create `tests/Functional/HomePageTest.php`: + +```php +requestFactory->createServerRequest('GET', '/'); + test_reset_runtime(); + } - $response = $this->handle($request); + public function testHomePageReturnsSuccessfulResponse(): void + { + $response = WebApp::handle(WebApp::request('GET', '/')); self::assertSame(200, $response->getStatusCode()); self::assertStringContainsString('Welcome', (string) $response->getBody()); } +} +``` - private function handle(ServerRequestInterface $request): ResponseInterface - { - $this->application->start(); +Run it: - try { - $response = $this->application->handle($request); - } finally { - $this->application->shutdown(); - } +```shell +vendor/bin/phpunit tests/Functional/HomePageTest.php +``` - $body = $response->getBody(); - if ($body->isSeekable()) { - $body->rewind(); - } +## Send request data - return $response; - } -} +Use PSR-7 methods to model the request: + +```php +$request = WebApp::request('POST', '/contact') + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withParsedBody([ + 'ContactForm' => [ + 'name' => 'Sam', + 'email' => 'sam@example.test', + 'body' => 'Hello.', + ], + ]); + +$response = WebApp::handle($request); + +self::assertSame(302, $response->getStatusCode()); +self::assertSame('/contact/sent', $response->getHeaderLine('Location')); ``` -The `bootstrap-web-test.php` file is project-specific. It should build the test container and return the -`Yiisoft\Yii\Http\Application` instance together with the PSR-17 server request factory used by the project. +For JSON APIs, write the JSON body into a PSR-7 stream and set `Content-Type: application/json`. ## Reset state -Functional tests usually touch more state than unit tests. Reset the database, cache, sessions, files, and outgoing -messages before the next request. If a test sends multiple requests, reset state only between scenarios that must be -independent. +Functional tests often touch runtime files, sessions, cache, and a database. Reset the changed state before each test: + +```php +protected function setUp(): void +{ + test_reset_runtime(); + // Reset database tables, cache pools, queues, and outgoing messages here. +} +``` -Use functional tests for behavior that needs application wiring. Keep pure domain rules in unit tests. +Use functional tests when the behavior depends on Yii wiring. Keep pure domain rules in unit tests. diff --git a/src/guide/testing/overview.md b/src/guide/testing/overview.md index b07a5264..2174eb95 100644 --- a/src/guide/testing/overview.md +++ b/src/guide/testing/overview.md @@ -1,49 +1,101 @@ # Testing -Tests help keep Yii applications and packages safe to change. Use the smallest test type that can prove the behavior: +This section shows a practical test setup for a Yii application. The examples use PHPUnit for unit and functional +tests. They keep Yii's HTTP tests close to the framework model: create a PSR-7 request, pass it to the application, and +assert on a PSR-7 response. + +Use the smallest test type that proves the behavior: 1. Unit tests. 2. Functional tests. 3. End-to-end tests. -This order keeps feedback fast. Unit tests are the cheapest to write and run. Functional tests cover application -integration through a real request and response. End-to-end tests exercise the application through an HTTP server or -browser and are best reserved for the flows that must work from the user's point of view. +Unit tests are fast and should cover most domain and service rules. Functional tests check application wiring through a +real request and response. End-to-end tests run through an HTTP server or browser and should cover only the main user +flows. + +## Set up the project + +Install PHPUnit: + +```shell +composer require --dev phpunit/phpunit +``` + +Create the test directories: + +```shell +mkdir -p tests/Unit tests/Functional tests/EndToEnd tests/Support +``` + +Add development autoloading and scripts to `composer.json`: + +```json +{ + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests" + } + }, + "scripts": { + "test": "phpunit", + "test:unit": "phpunit --testsuite Unit", + "test:functional": "phpunit --testsuite Functional" + } +} +``` + +Refresh Composer autoloading: + +```shell +composer dump-autoload +``` + +Follow [Testing environment setup](environment-setup.md) to add `phpunit.xml.dist`, a bootstrap file, and a state reset +helper. + +## Run tests + +Run the full PHPUnit suite: + +```shell +composer test +``` + +Run only unit tests: + +```shell +composer test:unit +``` -## Test types +Run only functional tests: -Unit tests check a small unit of code in isolation: a value object, service, middleware, handler, validator, or domain -operation. They should avoid bootstrapping the whole application when a direct object call is enough. +```shell +composer test:functional +``` -Functional tests run application code in the same PHP process. For web functionality, create a PSR-7 -`ServerRequestInterface`, pass it to the application or middleware stack, and assert on the returned PSR-7 -`ResponseInterface`. This tests routing, middleware, container configuration, and response creation without starting a -web server. +Run one test class or method: -End-to-end tests use the application the same way a user or external system uses it: through HTTP requests, a browser, -or another real client. They are useful for forms, authentication flows, JavaScript behavior, and integration with -services that are hard to represent in-process. +```shell +vendor/bin/phpunit tests/Functional/HomePageTest.php +vendor/bin/phpunit --filter testHomePageReturnsSuccessfulResponse +``` -## Application state +When the application runs in Docker, run the same command inside the test container: -Tests should start from a known application state and leave the application ready for the next test. Reset the same -state that a real application changes: +```shell +docker compose -f docker/compose.yml -f docker/test/compose.yml run --rm app vendor/bin/phpunit +``` -- Database rows. -- Cache entries. -- Sessions and cookies. -- Uploaded or generated files. -- Queues and outgoing messages. -- Time, random values, and other process-wide fakes. +## What to write first -Prefer a separate test environment, separate test database, and isolated runtime directories. A test that depends on -leftover data from a previous test can pass locally and fail in CI. +Start with unit tests for code that has no framework boundary: value objects, validators, domain services, and +transformers. -## Quality gates +Add functional tests for behavior that needs routing, middleware, configuration, a container definition, templates, or +session and cookie handling. -Automated tests are only one part of the feedback loop. Static analysis checks type contracts and unreachable or unsafe -code paths before the code runs. Mutation testing changes small parts of the source code and checks whether the test -suite fails; surviving mutations point to assertions that are missing or too weak. +Add end-to-end tests for user-visible workflows such as sign in, form submission, and JavaScript behavior. -Run unit and functional tests frequently during development. Run end-to-end tests, static analysis, and mutation testing -in CI or before releases, based on the project's size and cost of failure. +Run static analysis in CI and before changing shared contracts. Add mutation testing when the code is important enough +that weak assertions are a real risk. diff --git a/src/guide/testing/quality-tools.md b/src/guide/testing/quality-tools.md index 1265b9c0..9c377263 100644 --- a/src/guide/testing/quality-tools.md +++ b/src/guide/testing/quality-tools.md @@ -1,23 +1,113 @@ # Static analysis and mutation testing -Tests execute selected examples. Static analysis and mutation testing add different feedback. +Static analysis checks code without running it. Mutation testing changes small parts of the source code and checks +whether tests fail. Use both with the test suite. -Static analysis reads the code and checks type contracts, control flow, unreachable code, invalid calls, and other -issues before the code runs. Common PHP tools include [Psalm](https://psalm.dev/) and -[PHPStan](https://phpstan.org/). +## Static analysis -Mutation testing changes small parts of the source code and runs the test suite against each change. If the tests still -pass, the changed code is a surviving mutation. Surviving mutations often mean that assertions are missing, too broad, -or checking implementation details instead of behavior. A common PHP mutation testing tool is -[Infection](https://infection.github.io/). +Install Psalm: -## When to run them +```shell +composer require --dev vimeo/psalm +vendor/bin/psalm --init +``` -Run static analysis in CI for every pull request. Run it locally before pushing changes that affect shared interfaces, -container configuration, or generated types. +Run it: -Mutation testing is more expensive. Run it for packages, domain code, and critical services where test strength matters. -For large applications, start with a small source path and expand the scope as the suite becomes faster and more stable. +```shell +vendor/bin/psalm +``` -Use these tools with tests. Static analysis finds many type and flow problems. Mutation testing shows whether tests -would catch behavior changes. +Add a Composer script: + +```json +{ + "scripts": { + "psalm": "psalm --no-progress" + } +} +``` + +If the project uses PHPStan, install and configure it: + +```shell +composer require --dev phpstan/phpstan +``` + +Create `phpstan.neon`: + +```neon +parameters: + level: 6 + paths: + - src + - tests +``` + +Run it: + +```shell +vendor/bin/phpstan analyse +``` + +## Mutation testing + +Install Infection: + +```shell +composer require --dev infection/infection +``` + +Create `infection.json5`: + +```json5 +{ + "$schema": "vendor/infection/infection/resources/schema.json", + "source": { + "directories": [ + "src" + ] + }, + "phpUnit": { + "configDir": "." + }, + "logs": { + "text": "runtime/infection.log" + }, + "mutators": { + "@default": true + } +} +``` + +Run it: + +```shell +vendor/bin/infection --threads=max +``` + +For a large application, start with one path: + +```shell +vendor/bin/infection --filter=src/Shared +``` + +Add CI thresholds after the first clean run: + +```shell +vendor/bin/infection --threads=max --min-msi=80 --min-covered-msi=90 +``` + +## CI command + +A pull request check can run: + +```shell +composer install --no-interaction --prefer-dist +composer test +vendor/bin/psalm +vendor/bin/infection --threads=max --min-msi=80 --min-covered-msi=90 +``` + +Run mutation testing less often if it is too slow for every pull request. Static analysis and unit tests should run on +every change. diff --git a/src/guide/testing/unit.md b/src/guide/testing/unit.md index 63c9456e..1ead7a64 100644 --- a/src/guide/testing/unit.md +++ b/src/guide/testing/unit.md @@ -1,24 +1,40 @@ # Unit tests -Unit tests check a small piece of code directly. They are best for domain rules, pure services, value objects, data -transformers, middleware branches, validators, and error handling. +Unit tests check a small piece of code directly. They should be fast, deterministic, and independent of the application +container. -Keep unit tests focused: +## Create code to test -- Instantiate the class under test directly. -- Pass test doubles for dependencies that perform I/O. -- Assert the returned value, changed object state, or thrown exception. -- Cover one behavior per test method. +For example, create `src/Shared/SlugGenerator.php`: -Avoid loading the full application for code that can be tested with constructor arguments and method calls. +```php +clock->now()); + } +} +``` + +Create `tests/Unit/PublishPostTest.php`: + +```php +publish('Testing Yii'); + + self::assertSame('Testing Yii', $post->title); + self::assertSame('2026-05-07 10:00:00', $post->publishedAt->format('Y-m-d H:i:s')); + } +} +``` + +Run it: + +```shell +vendor/bin/phpunit tests/Unit/PublishPostTest.php +``` + +Unit tests should avoid real databases, HTTP calls, files, queues, and the full application bootstrap. If a behavior +needs those, write a functional test. From e762f518c2cfa73c70fff28899629cdd94b548ab Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 7 May 2026 20:03:05 +0300 Subject: [PATCH 4/4] Align testing guide with app template --- src/guide/testing/end-to-end.md | 140 ++++++++------ src/guide/testing/environment-setup.md | 254 +++++++++++++------------ src/guide/testing/functional.md | 86 ++++----- src/guide/testing/overview.md | 109 ++++++----- src/guide/testing/quality-tools.md | 125 ++++++------ src/guide/testing/unit.md | 72 +++++-- 6 files changed, 429 insertions(+), 357 deletions(-) diff --git a/src/guide/testing/end-to-end.md b/src/guide/testing/end-to-end.md index fc68add3..774981d2 100644 --- a/src/guide/testing/end-to-end.md +++ b/src/guide/testing/end-to-end.md @@ -1,95 +1,127 @@ # End-to-end tests -End-to-end tests run the application through a real HTTP server. Use them for user-visible flows such as sign in, form -submission, redirects, cookies, JavaScript behavior, and file uploads. +The Yii application template uses the `Web` Codeception suite for tests that go through an HTTP server. These tests live +in `tests/Web` and use `App\Tests\Support\WebTester`. + +Use web tests for user-visible HTTP behavior: pages, links, forms, redirects, cookies, and error pages. + +## Web suite + +The template configures `tests/Web.suite.yml` like this: + +```yaml +actor: WebTester +extensions: + enabled: + - Codeception\Extension\RunProcess: + 0: composer serve + sleep: 3 +modules: + enabled: + - PhpBrowser: + url: http://127.0.0.1:8080 +``` -Keep them few. Detailed business rules belong in unit and functional tests. +`RunProcess` starts the application with the Composer `serve` script. `PhpBrowser` sends HTTP requests to the server. -## Smoke test with curl +## Test a page -Start the application in the test environment: +The template includes `tests/Web/HomePageCest.php`: -```shell -APP_ENV=test ./yii serve --port=8080 +```php +wantTo('home page works.'); + $I->amOnPage('/'); + $I->expectTo('see page home.'); + $I->see('Hello!'); + } +} ``` -In another terminal, check the home page: +Run the web suite locally: ```shell -curl -fsS http://127.0.0.1:8080/ > /tmp/home.html -grep -q "Welcome" /tmp/home.html +APP_ENV=test vendor/bin/codecept run Web ``` -This is enough for a simple deployment or reverse-proxy smoke test. +Run only this test: -## Browser tests with Playwright +```shell +APP_ENV=test vendor/bin/codecept run Web HomePageCest +``` -Install Playwright: +With Docker: ```shell -npm install --save-dev @playwright/test -npx playwright install +make test Web ``` -Create `playwright.config.ts`: - -```ts -import { defineConfig } from '@playwright/test'; - -export default defineConfig({ - testDir: 'tests/EndToEnd', - webServer: { - command: 'APP_ENV=test ./yii serve --port=8080', - url: 'http://127.0.0.1:8080/', - reuseExistingServer: !process.env.CI, - }, - use: { - baseURL: 'http://127.0.0.1:8080', - }, -}); -``` +## Test links and error pages + +Use the same actor methods for navigation and response assertions: -Create `tests/EndToEnd/home-page.spec.ts`: +```php + { - await page.goto('/'); +namespace App\Tests\Web; - await expect(page).toHaveTitle(/Yii/i); - await expect(page.getByText('Welcome')).toBeVisible(); -}); +use App\Tests\Support\WebTester; + +final class NotFoundHandlerCest +{ + public function nonExistentPage(WebTester $I): void + { + $I->wantTo('see 404 page.'); + $I->amOnPage('/non-existent-page'); + $I->canSeeResponseCodeIs(404); + $I->see('404'); + $I->see('The page /non-existent-page not found.'); + } +} ``` -Run it: +## Smoke test with curl + +For a deployment smoke test, start the application and check one URL: ```shell -npx playwright test +APP_ENV=test ./yii serve --port=8080 ``` -Run it with a visible browser while debugging: +In another terminal: ```shell -npx playwright test --headed --debug +curl -fsS http://127.0.0.1:8080/ > /tmp/home.html +grep -q "Hello!" /tmp/home.html ``` ## Reset state -End-to-end tests use real infrastructure, so reset state before each scenario: +Web tests use real infrastructure, so reset state before each scenario: - Load only the records required by the scenario. - Clear session and cookie storage. - Clear generated files and outgoing messages. -- Stop background workers or make their effects deterministic. - -If the scenario changes a database, reset it in a Playwright `beforeEach` hook by calling a project-specific script: +- Stop background workers, or make their effects deterministic. -```ts -import { test } from '@playwright/test'; -import { execFileSync } from 'node:child_process'; +If the scenario changes a database, reset it in the Cest `_before()` hook or in a project-specific helper: -test.beforeEach(() => { - execFileSync('php', ['tests/reset-test-state.php']); -}); +```php +public function _before(WebTester $I): void +{ + // Reset database tables, files, queues, and outgoing messages. +} ``` diff --git a/src/guide/testing/environment-setup.md b/src/guide/testing/environment-setup.md index 6eef2604..9a0d5d6f 100644 --- a/src/guide/testing/environment-setup.md +++ b/src/guide/testing/environment-setup.md @@ -1,168 +1,188 @@ # Testing environment setup -Use a dedicated test environment. It should have its own environment variables, runtime directory, database, cache, and -service fakes. - -## Configure PHPUnit - -Create `phpunit.xml.dist` in the project root: - -```xml - - - - - tests/Unit - - - tests/Functional - - - - - - src - - - - - - - - -``` - -Create `tests/bootstrap.php`: - -```php - [ - 'file' => '@runtime/test/logs/app.log', - ], -]; +App\Environment::prepare(); ``` -Use the same approach for a test database, queue, mailer, or cache connection. Keep test credentials separate from -development credentials. +Set `APP_ENV=test` when running tests locally: -## Reset state +```shell +APP_ENV=test vendor/bin/codecept run +``` -Reset changed state in `setUp()` or in a project-specific base test case: +The Docker test service reads the same value from `docker/test/.env`: -```php -resetDatabase(); - $this->resetCache(); - } +`tests/Functional.suite.yml` uses the project `FunctionalTester` helper: - private function resetDatabase(): void - { - // Truncate tables, reload fixtures, or start a transaction. - } +```yaml +actor: FunctionalTester +``` - private function resetCache(): void - { - // Clear the cache storage used by the test environment. - } -} +`tests/Web.suite.yml` starts the built-in server and sends HTTP requests with PhpBrowser: + +```yaml +actor: WebTester +extensions: + enabled: + - Codeception\Extension\RunProcess: + 0: composer serve + sleep: 3 +modules: + enabled: + - PhpBrowser: + url: http://127.0.0.1:8080 ``` -Choose one database reset strategy and use it consistently: +`tests/Console.suite.yml` enables console command testing: -- Recreate the schema when tests need full isolation and the schema is small. -- Truncate tables and load fixtures for application-level tests. -- Use transactions for tests that stay on one database connection. +```yaml +actor: ConsoleTester +modules: + enabled: + - Cli +``` -Also reset sessions, cookies, uploaded files, queues, outgoing mail, fake clocks, and generated files when a test -changes them. +After adding or changing a suite, rebuild generated actor actions: -## Run locally and in Docker +```shell +vendor/bin/codecept build +``` -Run all tests locally: +With Docker: ```shell -APP_ENV=test vendor/bin/phpunit +make codecept build ``` -Run one suite: +## Test configuration + +Put test-only application parameters into `config/environments/test/params.php`. The application loads this file when +`APP_ENV=test`. + +Use the test environment for values such as: + +- Database name and credentials. +- Mailer transport. +- Queue transport. +- Cache storage. +- Log target paths. + +Keep test credentials separate from development and production credentials. + +## State reset + +Each test must leave the next test with predictable state. Choose a reset strategy for every external resource the test +changes: + +- Recreate or truncate database tables before application-level tests. +- Clear cache pools used by the test environment. +- Clear sessions and cookies in web tests. +- Remove generated files from `runtime`. +- Replace outgoing mail, queues, and HTTP clients with test doubles where practical. + +For database tests, prefer one project-wide fixture or migration flow. Mixing reset strategies makes failures hard to +reproduce. + +## Coverage + +Run coverage locally when Xdebug or another coverage driver is enabled: ```shell -APP_ENV=test vendor/bin/phpunit --testsuite Functional +APP_ENV=test APP_C3=true XDEBUG_MODE=coverage vendor/bin/codecept run --coverage --coverage-html --disable-coverage-php ``` -With Docker: +With Docker, coverage variables are already in `docker/test/.env`: ```shell -docker compose -f docker/compose.yml -f docker/test/compose.yml run --rm app vendor/bin/phpunit +make test-coverage ``` diff --git a/src/guide/testing/functional.md b/src/guide/testing/functional.md index af99840d..009f1645 100644 --- a/src/guide/testing/functional.md +++ b/src/guide/testing/functional.md @@ -1,14 +1,13 @@ # Functional tests -Functional tests run application code in the same PHP process. For a web application, create a PSR-7 request, pass it -to Yii, and assert on the PSR-7 response. +Functional tests run application code in the same PHP process. In the Yii application template they live in +`tests/Functional` and use `App\Tests\Support\FunctionalTester`. -This checks routing, middleware, action handlers, views, container configuration, headers, cookies, and sessions without -starting a web server. +Use them for routing, middleware, action handlers, views, container configuration, headers, cookies, and sessions. -## Add a web test helper +## Functional tester -Create `tests/Support/WebApp.php`: +The template already has a helper method for sending PSR-7 requests: ```php createServerRequest($method, $uri); - } + use _generated\FunctionalTesterActions; - public static function handle(ServerRequestInterface $request): ResponseInterface + public function sendRequest(ServerRequestInterface $request): ResponseInterface { $runner = new HttpApplicationRunner( rootPath: dirname(__DIR__, 2), - debug: true, - environment: Environment::TEST, - bootstrapGroup: 'bootstrap-web', - eventsGroup: 'events-web', - diGroup: 'di-web', - diProvidersGroup: 'di-providers-web', - diDelegatesGroup: 'di-delegates-web', - diTagsGroup: 'di-tags-web', - paramsGroup: 'params-web', - nestedParamsGroups: ['params'], - nestedEventsGroups: ['events'], + environment: Environment::appEnv(), ); $response = $runner->runAndGetResponse($request); $body = $response->getBody(); - if ($body->isSeekable()) { $body->rewind(); } @@ -61,11 +45,9 @@ final class WebApp } ``` -If your project uses different configuration group names, take them from `config/configuration.php`. - ## Test a page -Create `tests/Functional/HomePageTest.php`: +The template includes `tests/Functional/HomePageCest.php`: ```php sendRequest( + new ServerRequest(uri: '/'), + ); - self::assertSame(200, $response->getStatusCode()); - self::assertStringContainsString('Welcome', (string) $response->getBody()); + assertSame(200, $response->getStatusCode()); + assertStringContainsString( + 'Don\'t forget to check the guide', + $response->getBody()->getContents(), + ); } } ``` @@ -97,7 +82,8 @@ final class HomePageTest extends TestCase Run it: ```shell -vendor/bin/phpunit tests/Functional/HomePageTest.php +APP_ENV=test vendor/bin/codecept run Functional +APP_ENV=test vendor/bin/codecept run Functional HomePageCest ``` ## Send request data @@ -105,7 +91,7 @@ vendor/bin/phpunit tests/Functional/HomePageTest.php Use PSR-7 methods to model the request: ```php -$request = WebApp::request('POST', '/contact') +$request = (new ServerRequest(uri: '/contact', method: 'POST')) ->withHeader('Content-Type', 'application/x-www-form-urlencoded') ->withParsedBody([ 'ContactForm' => [ @@ -115,24 +101,24 @@ $request = WebApp::request('POST', '/contact') ], ]); -$response = WebApp::handle($request); +$response = $tester->sendRequest($request); -self::assertSame(302, $response->getStatusCode()); -self::assertSame('/contact/sent', $response->getHeaderLine('Location')); +assertSame(302, $response->getStatusCode()); +assertSame('/contact/sent', $response->getHeaderLine('Location')); ``` For JSON APIs, write the JSON body into a PSR-7 stream and set `Content-Type: application/json`. ## Reset state -Functional tests often touch runtime files, sessions, cache, and a database. Reset the changed state before each test: +Functional tests often touch runtime files, sessions, cache, and a database. Reset changed state in the Cest `_before()` +hook or in a project helper: ```php -protected function setUp(): void +public function _before(FunctionalTester $tester): void { - test_reset_runtime(); - // Reset database tables, cache pools, queues, and outgoing messages here. + // Reset database tables, cache pools, queues, and outgoing messages. } ``` -Use functional tests when the behavior depends on Yii wiring. Keep pure domain rules in unit tests. +Keep pure domain rules in unit tests. Put request and response behavior in functional tests. diff --git a/src/guide/testing/overview.md b/src/guide/testing/overview.md index 2174eb95..dcb654a5 100644 --- a/src/guide/testing/overview.md +++ b/src/guide/testing/overview.md @@ -1,101 +1,98 @@ # Testing -This section shows a practical test setup for a Yii application. The examples use PHPUnit for unit and functional -tests. They keep Yii's HTTP tests close to the framework model: create a PSR-7 request, pass it to the application, and -assert on a PSR-7 response. +The Yii application template includes a ready test setup. It uses Codeception with PHPUnit assertions and has four +suites: -Use the smallest test type that proves the behavior: +- `Unit` for isolated PHP classes. +- `Functional` for application code called in the same PHP process. +- `Web` for requests sent through an HTTP server. +- `Console` for console commands. -1. Unit tests. -2. Functional tests. -3. End-to-end tests. +The main files are: -Unit tests are fast and should cover most domain and service rules. Functional tests check application wiring through a -real request and response. End-to-end tests run through an HTTP server or browser and should cover only the main user -flows. +- `codeception.yml`. +- `tests/bootstrap.php`. +- `tests/Unit.suite.yml`. +- `tests/Functional.suite.yml`. +- `tests/Web.suite.yml`. +- `tests/Console.suite.yml`. +- `tests/Support/*Tester.php`. -## Set up the project +## Run tests locally -Install PHPUnit: +Build actor classes after installing dependencies or changing a suite: ```shell -composer require --dev phpunit/phpunit +vendor/bin/codecept build ``` -Create the test directories: +Run all tests: ```shell -mkdir -p tests/Unit tests/Functional tests/EndToEnd tests/Support +APP_ENV=test vendor/bin/codecept run ``` -Add development autoloading and scripts to `composer.json`: - -```json -{ - "autoload-dev": { - "psr-4": { - "App\\Tests\\": "tests" - } - }, - "scripts": { - "test": "phpunit", - "test:unit": "phpunit --testsuite Unit", - "test:functional": "phpunit --testsuite Functional" - } -} -``` - -Refresh Composer autoloading: +The template also defines a Composer script: ```shell -composer dump-autoload +APP_ENV=test composer test ``` -Follow [Testing environment setup](environment-setup.md) to add `phpunit.xml.dist`, a bootstrap file, and a state reset -helper. - -## Run tests - -Run the full PHPUnit suite: +Run one suite: ```shell -composer test +APP_ENV=test vendor/bin/codecept run Unit +APP_ENV=test vendor/bin/codecept run Functional +APP_ENV=test vendor/bin/codecept run Web +APP_ENV=test vendor/bin/codecept run Console ``` -Run only unit tests: +Run one test class or method: ```shell -composer test:unit +APP_ENV=test vendor/bin/codecept run Functional HomePageCest +APP_ENV=test vendor/bin/codecept run Functional HomePageCest:base ``` -Run only functional tests: +`APP_ENV=test` is required for local commands because `tests/bootstrap.php` prepares the application environment before +tests run. + +## Run tests in Docker + +Build actor classes: ```shell -composer test:functional +make codecept build ``` -Run one test class or method: +Run all tests: ```shell -vendor/bin/phpunit tests/Functional/HomePageTest.php -vendor/bin/phpunit --filter testHomePageReturnsSuccessfulResponse +make test ``` -When the application runs in Docker, run the same command inside the test container: +Run one suite: ```shell -docker compose -f docker/compose.yml -f docker/test/compose.yml run --rm app vendor/bin/phpunit +make test Unit +make test Functional +make test Web +make test Console ``` -## What to write first +The Docker test environment reads `docker/test/.env`, where `APP_ENV=test` is already set. + +## Choose a suite Start with unit tests for code that has no framework boundary: value objects, validators, domain services, and transformers. -Add functional tests for behavior that needs routing, middleware, configuration, a container definition, templates, or -session and cookie handling. +Use functional tests when code needs Yii configuration, dependency injection, routing, middleware, request handling, or +template rendering. + +Use web tests for behavior that must go through an HTTP server: status codes, links, redirects, cookies, and rendered +pages as seen by a client. -Add end-to-end tests for user-visible workflows such as sign in, form submission, and JavaScript behavior. +Use console tests for commands in `src/Console`. -Run static analysis in CI and before changing shared contracts. Add mutation testing when the code is important enough -that weak assertions are a real risk. +Run static analysis in CI and before changing shared contracts. diff --git a/src/guide/testing/quality-tools.md b/src/guide/testing/quality-tools.md index 9c377263..6fd08618 100644 --- a/src/guide/testing/quality-tools.md +++ b/src/guide/testing/quality-tools.md @@ -1,113 +1,106 @@ -# Static analysis and mutation testing +# Static analysis and code quality -Static analysis checks code without running it. Mutation testing changes small parts of the source code and checks -whether tests fail. Use both with the test suite. +The Yii application template includes static analysis and code quality tools alongside the test suite. -## Static analysis +## Psalm -Install Psalm: +Run Psalm locally: ```shell -composer require --dev vimeo/psalm -vendor/bin/psalm --init +vendor/bin/psalm ``` -Run it: +Run Psalm in Docker: ```shell -vendor/bin/psalm +make psalm ``` -Add a Composer script: +The template stores Psalm configuration in `psalm.xml`. The GitHub Actions workflow runs Psalm for supported PHP +versions. -```json -{ - "scripts": { - "psalm": "psalm --no-progress" - } -} -``` +When Psalm reports an issue, fix the code or add a precise type annotation. Keep suppressions narrow and local to the +line or method that needs them. -If the project uses PHPStan, install and configure it: +## Composer Dependency Analyser -```shell -composer require --dev phpstan/phpstan -``` +Composer Dependency Analyser checks that `composer.json` matches the classes used by the application. -Create `phpstan.neon`: +Run it locally: -```neon -parameters: - level: 6 - paths: - - src - - tests +```shell +vendor/bin/composer-dependency-analyser --config=composer-dependency-analyser.php ``` -Run it: +Run it in Docker: ```shell -vendor/bin/phpstan analyse +make composer-dependency-analyser ``` -## Mutation testing +Use this check after adding or removing package usage in `src`, `config`, or `tests`. + +## PHP CS Fixer -Install Infection: +PHP CS Fixer applies the project coding style from `.php-cs-fixer.php`. + +Run it locally: ```shell -composer require --dev infection/infection +vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff ``` -Create `infection.json5`: - -```json5 -{ - "$schema": "vendor/infection/infection/resources/schema.json", - "source": { - "directories": [ - "src" - ] - }, - "phpUnit": { - "configDir": "." - }, - "logs": { - "text": "runtime/infection.log" - }, - "mutators": { - "@default": true - } -} +Run it in Docker: + +```shell +make cs-fix ``` -Run it: +Commit formatting changes together with the code that needs them. + +## Rector + +Rector applies configured code upgrades and refactorings from `rector.php`. + +Preview changes locally: ```shell -vendor/bin/infection --threads=max +vendor/bin/rector --dry-run ``` -For a large application, start with one path: +Apply changes locally: ```shell -vendor/bin/infection --filter=src/Shared +vendor/bin/rector ``` -Add CI thresholds after the first clean run: +Run it in Docker: ```shell -vendor/bin/infection --threads=max --min-msi=80 --min-covered-msi=90 +make rector ``` -## CI command +Review Rector changes before committing them. Automated refactoring can change behavior when custom rules or broad paths +are configured. -A pull request check can run: +## Pull request checks + +A practical local check before opening a pull request is: ```shell -composer install --no-interaction --prefer-dist -composer test +APP_ENV=test vendor/bin/codecept build +APP_ENV=test vendor/bin/codecept run vendor/bin/psalm -vendor/bin/infection --threads=max --min-msi=80 --min-covered-msi=90 +vendor/bin/composer-dependency-analyser --config=composer-dependency-analyser.php +vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff ``` -Run mutation testing less often if it is too slow for every pull request. Static analysis and unit tests should run on -every change. +With Docker: + +```shell +make codecept build +make test +make psalm +make composer-dependency-analyser +make cs-fix +``` diff --git a/src/guide/testing/unit.md b/src/guide/testing/unit.md index 1ead7a64..f9cb8450 100644 --- a/src/guide/testing/unit.md +++ b/src/guide/testing/unit.md @@ -1,9 +1,49 @@ # Unit tests -Unit tests check a small piece of code directly. They should be fast, deterministic, and independent of the application -container. +Unit tests check a small piece of PHP code directly. In the Yii application template they live in `tests/Unit` and run +through the `Unit` Codeception suite. -## Create code to test +The template includes `tests/Unit/EnvironmentTest.php`: + +```php +generate('Hello, Yii3!'); - self::assertSame('hello-yii3', $slug); + assertSame('hello-yii3', $slug); } } ``` @@ -53,7 +95,7 @@ final class SlugGeneratorTest extends TestCase Run it: ```shell -vendor/bin/phpunit tests/Unit/SlugGeneratorTest.php +APP_ENV=test vendor/bin/codecept run Unit SlugGeneratorTest ``` ## Test services with dependencies @@ -134,10 +176,12 @@ namespace App\Tests\Unit; use App\Clock\ClockInterface; use App\Post\PublishPost; +use Codeception\Test\Unit; use DateTimeImmutable; -use PHPUnit\Framework\TestCase; -final class PublishPostTest extends TestCase +use function PHPUnit\Framework\assertSame; + +final class PublishPostTest extends Unit { public function testPublishSetsPublicationDate(): void { @@ -151,8 +195,8 @@ final class PublishPostTest extends TestCase $service = new PublishPost($clock); $post = $service->publish('Testing Yii'); - self::assertSame('Testing Yii', $post->title); - self::assertSame('2026-05-07 10:00:00', $post->publishedAt->format('Y-m-d H:i:s')); + assertSame('Testing Yii', $post->title); + assertSame('2026-05-07 10:00:00', $post->publishedAt->format('Y-m-d H:i:s')); } } ``` @@ -160,8 +204,8 @@ final class PublishPostTest extends TestCase Run it: ```shell -vendor/bin/phpunit tests/Unit/PublishPostTest.php +APP_ENV=test vendor/bin/codecept run Unit PublishPostTest ``` -Unit tests should avoid real databases, HTTP calls, files, queues, and the full application bootstrap. If a behavior -needs those, write a functional test. +Keep unit tests focused on one class or one small collaboration. Use functional tests when the behavior depends on Yii +configuration, dependency injection, routing, or middleware.