pestphp/pest · error · TestClosureMustNotBeStatic
Test closures may not be static. Please remove the [static]
Error message
Test closures may not be static. Please remove the [static] keyword from the test [%s] in [%s].
What it means
Pest binds test closures to the test case instance so $this (datasets via constructor promotion, $this->... helpers, beforeEach state) works. Static closures have no bound scope, so TestCaseFactory::addMethod rejects them with TestClosureMustNotBeStatic, naming the test and file. It is a load-time error: the test never runs.
Source
Thrown at src/Factories/TestCaseFactory.php:195
}
}
public function addMethod(TestCaseMethodFactory $method): void
{
if ($method->description === null) {
throw new TestDescriptionMissing($method->filename);
}
if (array_key_exists($method->description, $this->methods)) {
throw new TestAlreadyExist($method->filename, $method->description);
}
if (
$method->closure instanceof \Closure &&
new \ReflectionFunction($method->closure)->isStatic()
) {
throw new TestClosureMustNotBeStatic($method);
}
if (! $method->receivesArguments()) {
if (! $method->closure instanceof \Closure) {
throw ShouldNotHappen::fromMessage('The test closure may not be empty.');
}
$arguments = Reflection::getFunctionArguments($method->closure);
if ($arguments !== []) {
throw new DatasetMissing($method->filename, $method->description, $arguments);
}
}
$this->methods[$method->description] = $method;
}
public function hasMethod(string $methodName): boolView on GitHub (pinned to 1af74a215c)
Solutions
- Remove the static keyword from the test closure (the message names the test and file).
- Configure your fixer to exclude tests: e.g., in Pint/php-cs-fixer, disable the static_lambda or native_function_invocation-style rules for tests/ or set a finder that skips Pest files.
- If the closure must stay reusable, move the logic to a named helper and call it from a non-static test closure.
Example fix
// before
test('calculates total', static function () {
expect(total(1, 2))->toBe(3);
});
// after
test('calculates total', function () {
expect(total(1, 2))->toBe(3);
}); Defensive patterns
Strategy: validation
Validate before calling
// php-cs-fixer: exclude tests from static-lambda fixes (pest.hjson or .php-cs-fixer.php)
->setRules([
'static_lambda' => false,
])
->in([__DIR__.'/app', __DIR__.'/src']) // tests/ deliberately not included Type guard
function isStaticClosure(Closure $closure): bool
{
return (new ReflectionFunction($closure))->isStatic();
}
// usage in generators:
if (isStaticClosure($closure)) {
$closure = Closure::bind($closure, null, $scope) ?? $closure; // or reject: static not allowed
} Prevention
- Never write static closures in test files — Pest binds them to the test case for $this support.
- Scope static-lambda/style fixers to app code, not tests/.
- If a helper returns closures for tests, assert they are non-static in a meta-test.
When it happens
Trigger: test('works', static function () { ... }); it('...', static fn () => ...); refactoring service code into tests and leaving the static keyword; IDE auto-import or a coding standard (static analysis fixers) adding static to closures for performance, catching test closures too.
Common situations: Running php-cs-fixer / Pint / a 'static closures' fixer rule over the tests directory; copying static closures from production code into tests; developers optimizing closure instantiation without realizing Pest relies on binding ($this->set(...), $this->name, etc.).
Related errors
- A test in [%s] is missing its description. Please give every
- A test named [%s] already exists in [%s]. Please give this t
- The test [%s] in [%s] expects [%d] argument(s) ([%s]), but n
- The test directory [%s] does not exist.
- Cached failure
AI-assisted analysis of pestphp/pest@1af74a215c (2026-08-21).
Data as JSON: /api/errors/fd03b0437f1362f0.
Report an issue: GitHub.