Compare commits

...

4 Commits

Author SHA1 Message Date
Caleb White fc17365bd1 fix(tia): suppress PHPUnit XML coverage during recording (#1845)
When phpunit.xml has <coverage> and <source> sections, PHPUnit
auto-initializes its code coverage driver which takes over
xdebug's coverage APIs. TIA's recorder then gets empty data
from xdebug_get_code_coverage(), resulting in zero recorded
edges and no graph being saved.

Inject --no-coverage into the arguments when TIA enters its
own recording mode (not piggybacking on an explicit coverage
report). This tells PHPUnit to ignore XML-configured coverage
reports, leaving xdebug free for TIA's per-test recording.

Having <coverage> and <source> in phpunit.xml is standard for
any project that generates coverage reports. Without this fix,
users must manually pass --no-coverage alongside --tia, which
is an unnecessary footgun.
2026-08-26 01:25:57 +05:30
Lazizbek Ergashev 20a5aacaca fix(tia): re-anchor after a full suite run without a coverage driver (#1866)
Co-authored-by: Punyapal Shah <53343069+MrPunyapal@users.noreply.github.com>
2026-08-25 23:45:02 +05:30
nuno maduro eac05bcee9 release: 5.1.3 2026-08-25 18:10:42 +01:00
Pushpak Chhajed b5b2ba7d7f chore: drop boost pest-testing skill (#1889)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 16:42:49 +01:00
8 changed files with 79 additions and 200 deletions
+1
View File
@@ -30,6 +30,7 @@
},
"conflict": {
"filp/whoops": "<2.18.3",
"laravel/boost": "<2.6.0",
"phpunit/phpunit": ">13.3.1",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
@@ -1,196 +0,0 @@
---
name: pest-testing
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, architecture tests, or faster test runs with Test Impact Analysis. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, Tia (--tia), sharding, and all Pest 5 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
license: MIT
metadata:
author: laravel
---
@php
/** @var \Laravel\Boost\Install\GuidelineAssist $assist */
@endphp
# Pest Testing 5
## Documentation
Use `search-docs` for detailed Pest 5 patterns and documentation.
## Basic Usage
### Creating Tests
All tests must be written using Pest. Use `{{ $assist->artisanCommand('make:test --pest {name}') }}`.
The `{name}` argument should include only the path and test name, but should not include the test suite.
- Incorrect: `{{ $assist->artisanCommand('make:test --pest Feature/SomeFeatureTest') }}` will generate `tests/Feature/Feature/SomeFeatureTest.php`
- Correct: `{{ $assist->artisanCommand('make:test --pest SomeControllerTest') }}` will generate `tests/Feature/SomeControllerTest.php`
- Incorrect: `{{ $assist->artisanCommand('make:test --pest --unit Unit/SomeServiceTest') }}` will generate `tests/Unit/Unit/SomeServiceTest.php`
- Correct: `{{ $assist->artisanCommand('make:test --pest --unit SomeServiceTest') }}` will generate `tests/Unit/SomeServiceTest.php`
### Test Organization
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.
### Basic Test Structure
Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.
@boostsnippet("Basic Pest Test Example", "php")
it('is true', function () {
expect(true)->toBeTrue();
});
@endboostsnippet
### Running Tests
- Run minimal tests with filter before finalizing: `{{ $assist->artisanCommand('test --compact --filter=testName') }}`.
- Run all tests: `{{ $assist->artisanCommand('test --compact') }}`.
- Run file: `{{ $assist->artisanCommand('test --compact tests/Feature/ExampleTest.php') }}`.
- Run only tests affected by recent changes (Tia): `./vendor/bin/pest --parallel --tia`.
## Assertions
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
@boostsnippet("Pest Response Assertion", "php")
it('returns all', function () {
$this->postJson('/api/docs', [])->assertSuccessful();
});
@endboostsnippet
| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |
## Mocking
Import mock function before use: `use function Pest\Laravel\mock;`
## Datasets
Use datasets for repetitive tests (validation rules, etc.):
@boostsnippet("Pest Dataset Example", "php")
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
@endboostsnippet
## Pest 5 Features
| Feature | Purpose |
|---------|---------|
| Tia (Test Impact Analysis) | Rerun only tests affected by recent changes |
| Time-Balanced Sharding | Split tests across CI shards by execution time |
| New Validation Expectations | `toBeEmail()`, `toBeUlid()`, `toBeIpAddress()`, and more |
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Architecture Testing | Enforce code conventions |
### Tia (Test Impact Analysis)
Tia reruns only tests affected by recent changes and replays cached results for the rest, dramatically reducing suite duration:
@boostsnippet("Tia Example", "shell")
./vendor/bin/pest --parallel --tia
@endboostsnippet
- Replayed tests are not skipped — cached tests store everything they produced, including covered lines and branches.
- Detects Laravel, Symfony, Livewire, and Inertia automatically.
### New Validation Expectations
Pest 5 ships eight new validation matchers, all supporting `.not` negation:
@boostsnippet("Pest 5 Validation Expectations", "php")
expect('nuno@pestphp.com')->toBeEmail();
expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid();
expect('192.168.1.1')->toBeIpAddress();
expect('00:1a:2b:3c:4d:5e')->toBeMacAddress();
expect('example.com')->toBeHostname();
expect('example.co.uk')->toBeDomain();
expect('Zm9vYmFy')->toBeBase64();
expect('deadbeef')->toBeHexadecimal();
@endboostsnippet
### Time-Balanced Sharding
Distribute tests across CI shards by execution time rather than count:
@boostsnippet("Pest Sharding Example", "shell")
./vendor/bin/pest --update-shards
./vendor/bin/pest --shard=1/4
@endboostsnippet
Commit `tests/.pest/shards.json` to the repository so CI shards stay consistent.
### Browser Test Example
Browser tests run in real browsers for full integration testing:
- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.
@boostsnippet("Pest Browser Test Example", "php")
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in');
$page->assertSee('Sign In')
->assertNoJavaScriptErrors()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!');
Notification::assertSent(ResetPassword::class);
});
@endboostsnippet
### Smoke Testing
Quickly validate multiple pages have no JavaScript errors:
@boostsnippet("Pest Smoke Testing Example", "php")
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
@endboostsnippet
### Visual Regression Testing
Capture and compare screenshots to detect visual changes.
### Architecture Testing
@boostsnippet("Architecture Test Example", "php")
arch('controllers')
->expect('App\Http\Controllers')
->toExtendNothing()
->toHaveSuffix('Controller');
@endboostsnippet
## Common Pitfalls
- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '5.1.2';
return '5.1.3';
}
function testDirectory(string $file = ''): string
+9 -1
View File
@@ -200,6 +200,8 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
private bool $graphUnreachable = false;
private bool $fullSuiteFallbackRan = false;
/** @var array<int, string> */
private array $originalArguments = [];
@@ -684,7 +686,7 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
return $exitCode;
}
if ($this->replayRan || $this->graphUnreachable) {
if ($this->replayRan || $this->graphUnreachable || $this->fullSuiteFallbackRan) {
$this->bumpRecordedSha();
}
@@ -1044,6 +1046,8 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
$coverageAvailable = $this->piggybackCoverage || $this->recorder->driverAvailable();
if ($hasProjectPhpSourceChanges && ! $coverageAvailable) {
$this->fullSuiteFallbackRan = true;
$this->renderBadge('WARN', 'Detected PHP source changes but no coverage driver is available.');
$this->renderChild('Running the full suite to avoid using a stale dependency graph.');
$this->renderChild('Install / enable pcov or xdebug (mode: coverage) so edges can be safely refreshed after PHP refactors.');
@@ -1234,6 +1238,10 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
return $arguments;
}
if (! $this->piggybackCoverage && ! in_array('--no-coverage', $arguments, true)) {
$arguments[] = '--no-coverage';
}
if (Parallel::isEnabled()) {
$this->purgeWorkerPartials();
@@ -1,5 +1,5 @@
Pest Testing Framework 5.1.2.
Pest Testing Framework 5.1.3.
USAGE: pest <file> [options]
@@ -1,3 +1,3 @@
Pest Testing Framework 5.1.2.
Pest Testing Framework 5.1.3.
+39
View File
@@ -2,6 +2,7 @@
declare(strict_types=1);
use Pest\Support\Coverage;
use Tests\Fixtures\Tia\Project;
afterEach(function (): void {
@@ -39,6 +40,44 @@ test('a plain run after a coverage run records the whole project scope', functio
->and($graph['files'])->toContain('app/Calculator.php');
})->skipOnWindows();
test('xml-configured coverage does not prevent tia recording', function (): void {
$project = Project::make('master');
$project->write('phpunit.xml', <<<'XML_WRAP'
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
colors="true"
failOnRisky="true"
failOnWarning="false"
>
<testsuites>
<testsuite name="default">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<coverage>
<report>
<clover outputFile="coverage/clover.xml" />
</report>
</coverage>
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
</source>
</phpunit>
XML_WRAP);
$result = $project->pest('--tia');
expect($result->exitCode)->toBe(0, $result->describe())
->and($project->graphExists())->toBeTrue()
->and(array_keys($project->graph()['edges']))->toEqualCanonicalizing(array_keys(Project::EDGES));
})->skipOnWindows()->skip(! Coverage::isAvailable(), 'Coverage is not available');
test('a coverage report leaves the edges of an existing graph alone', function (): void {
$project = Project::make('master');
$project->seed('master');
+27
View File
@@ -358,3 +358,30 @@ test('a graph whose recorded commit is gone is re-anchored, not warned about for
->and($second->replayed())->toBe(Project::TOTAL_TESTS, $second->describe())
->and($delta->writtenCount())->toBe(0, $delta->summary());
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
test('a full suite run without a coverage driver clears the tree it could not refresh', function (array $arguments): void {
$project = Project::make('master');
$project->seed('master');
$project->mutateGraph(function (array $graph): array {
$graph['baselines']['master']['tree'] = ['app/Calculator.php' => 'deadbeefdeadbeefdeadbeefdeadbeef'];
return $graph;
});
$environment = ['XDEBUG_MODE' => 'off'];
$first = $project->pestWithEnvironment($project->path(), $environment, '--tia', ...$arguments);
expect($first->exitCode)->toBe(0, $first->describe())
->and($first->output)->toContain('no coverage driver is available')
->and($first->tally())->toContain(Project::TOTAL_TESTS.' passed');
$project->snapshot();
$second = $project->pestWithEnvironment($project->path(), $environment, '--tia', ...$arguments);
$delta = $project->delta();
expect($second->output)->not->toContain('no coverage driver is available')
->and($second->replayed())->toBe(Project::TOTAL_TESTS, $second->describe())
->and($delta->writtenCount())->toBe(0, $delta->summary());
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();