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.

Solutions

  1. Remove the static keyword from the test closure (the message names the test and file).
  2. 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.
  3. 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

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


AI-assisted analysis of pestphp/pest@1af74a215c (2026-08-21). Data as JSON: /api/errors/fd03b0437f1362f0. Report an issue: GitHub.

Appendix: 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): bool

View on GitHub (pinned to 1af74a215c)