heyjordanparker/mago-pest
| Install | |
|---|---|
composer require heyjordanparker/mago-pest |
|
| Latest Version: | 0.4.0 |
| PHP: | ^8.3 |
| License: | MIT |
| Last Updated: | Oct 11, 2026 |
| Links: | GitHub · Packagist |
Mago Pest
A mago-sharp analyzer extension that types Pest test files the way Pest runs them.
Without it, Mago reads a Pest test file as plain PHP. It cannot see the test case class that $this refers to inside a test closure, so every call to a test case method or helper trait, and every property a beforeEach hook sets, is reported. Expectation chains are reported too, because Pest dispatches most of them through __call and __get.
With it, Mago analyzes:
$thisintest(),it(),beforeEach(), andafterEach()closures, and in thebeforeEach()andafterEach()hooks ofpest()anduses(), as the test case Pest builds frompest()->extend(),pest()->use(),uses(), and theirin()targets.- Properties that those closures assign to
$this, typed by the assigned values. - Matcher chains:
expect($value)->toBe(1)->not->toBeNull()->each->toBeInt(). - Higher-order expectations:
expect($user)->name->toBe('Ada')andexpect($user)->domain()->toBe('example.com'). - Custom matchers from
expect()->extend('name', closure), with the closure's parameters. toThrow()with a closure typed by the exception it expects.
Install
composer require --dev heyjordanparker/mago-sharp:^0.3 heyjordanparker/mago-pest
mago-pest runs only on mago-sharp 0.3. Composer treats each 0.x minor as a breaking release, so ^0.3 does not pick up 0.4.
The project owns its worker entrypoint. Create .mago/extensions.php:
<?php
declare(strict_types=1);
use Mago\Sdk\Worker;
use MagoPest\PestExtension;
require dirname(__DIR__) . '/vendor/autoload.php';
(new Worker(PestExtension::create()))->run();
Register it in mago.toml:
[extension-hosts.pest]
command = ["php", ".mago/extensions.php"]
PestExtension::create() takes one option, testDirectory. It is Pest's test directory relative to the Mago workspace, and it defaults to tests:
(new Worker(PestExtension::create(testDirectory: 'src/Tests')))->run();
A worker can host several extensions:
(new Worker(App\Mago\Extension::create(), PestExtension::create()))->run();
How it works
Before Mago parses the project, the extension reads every PHP file under the test directory. For each test file it declares the class Pest generates, P\<path>, with the same parent class and traits Pest gives it. Each pest() or uses() declaration with hooks gets a class of its own, and test files share the properties those hooks assign.
Pest marks its generated class #[AllowDynamicProperties]. Mago does not model that attribute, so the declared class has __get and __set instead. Mago therefore reports a property that no closure assigns as non-documented-property, not non-existent-property.
A property's type is the union of every value assigned to it. The extension reads each value from its tokens, so it can type only a value whose tokens state the type:
- a literal, a string, an array, or a cast
newof a named class- a closure, typed by its own signature
- one call to a function, a static method, or a method on
$this, typed by its declared return type
Any other value is mixed. That includes a variable, an array access, a chained call, an arithmetic expression, and a return type that depends on templates. A property assigned any mixed value is mixed.
Limits
- Closures in a
describe()body outside a test or hook do not run on a test case, and Pest does not bind$thisin them. Mago reports$thisthere, as it should. Closure::bind()andClosure::call()rebind$thisat runtime, and the extension does not follow them.- A
pest()oruses()declaration is read only when its classes are given asName::classor as a string, and itsin()targets as strings or__DIR__.
Development
composer install
just check
just check validates composer.json, then checks formatting, lints, analyzes the source, runs the corpus, and runs the unit tests in tests/Unit. The corpus in tests/corpus runs the real worker over Pest test files and checks every inline @mago-expect annotation. A test in it that passes shows a capability, and an annotated line shows a defect the extension still reports.
License
MIT
Related Packages
PHPStan extensions to infer Pest closure $this type from test file mappings.
Psalm plugin for Pest: types $this in test closures as the configured TestCase (...
Method-level and class-level maintainability limits (complexity, body lines, par...
Version History
| Version | Released | PHP | License |
|---|---|---|---|
| 0.4.0 | ^8.3 | MIT | |
| 0.3.0 | ^8.3 | MIT | |
| 0.2.1 | ^8.3 | MIT | |
| 0.2.0 | ^8.3 | MIT | |
| 0.1.0 | ^8.3 | MIT |