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

View on GitHub (pinned to 1af74a215c)

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.