sebastianbergmann/comparator · error · ComparisonFailure

Failed asserting that closure declared at

Error message

Failed asserting that closure declared at %s:%d is equal to closure declared at %s:%d.

What it means

ComparisonFailure thrown by ClosureComparator::assertEquals whenever two closures are compared. Closures are considered equal only if they are the same object; the message reports the file:line where each closure was declared. The exported strings are opaque 'Closure Object #id ()' placeholders.

Solutions

  1. Compare closure identity with === (same instance) before asserting equality, or assert against the very same closure instance.
  2. Instead of comparing closures, assert observable behavior: invoke both with sample arguments and compare return values.
  3. If you control production code, inject a named invokable class or string callable instead of two separate closure literals.
  4. Catch ComparisonFailure and inspect the declaration file:line in the message when the mismatch itself is the subject of the test.

Example fix

// before
$c = new Factory; $c->getComparatorFor($f, $g)->assertEquals($f, $g); // always throws for distinct closures
// after
$this->assertSame($f, $f); // identity, or: assertEquals($f($in), $g($in)); // compare behavior
Defensive patterns

Strategy: type-guard

Validate before calling

// only compare when identity equality is impossible/irrelevant
if ($expected === $actual) { /* equal, skip comparator */ }

Type guard

function isClosure(mixed $v): bool { return $v instanceof Closure; }

Try / catch

try {
    $factory->getComparatorFor($f, $g)->assertEquals($f, $g);
} catch (ComparisonFailure $e) {
    // closures are never equal unless identical; parse declaration location from message
}

Prevention

When it happens

Trigger: Any assertEquals($expectedClosure, $actualClosure) invocation through the comparator Factory where the two closures are distinct objects — even if the bodies are identical source code, they differ because spl_object_id / declaration location differ.

Common situations: PHPUnit tests asserting a callback was equal to an expected closure; passing anonymous functions through config and comparing them; identity checks of closures captured from different call sites (each literal creates a new Closure object, so identical code at two lines never matches).

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of sebastianbergmann/comparator@00837a9d22 (2026-09-15). Data as JSON: /api/errors/85f80fcbe1713061. Report an issue: GitHub.

Appendix: source

Thrown at src/ClosureComparator.php:69

        $actualReflector   = new ReflectionFunction($actual);

        if ($this->declarationIsEqual($expectedReflector, $actualReflector) &&
            $this->bindingIsEqual($expectedReflector, $actualReflector) &&
            $this->capturedStateIsEqual($expectedReflector, $actualReflector)) {
            return;
        }

        $expectedFilename  = $expectedReflector->getFileName();
        $expectedStartLine = $expectedReflector->getStartLine();
        $actualFilename    = $actualReflector->getFileName();
        $actualStartLine   = $actualReflector->getStartLine();

        assert($expectedFilename !== false);
        assert($expectedStartLine !== false);
        assert($actualFilename !== false);
        assert($actualStartLine !== false);

        throw new ComparisonFailure(
            $expected,
            $actual,
            'Closure Object #' . spl_object_id($expected) . ' ()',
            'Closure Object #' . spl_object_id($actual) . ' ()',
            sprintf(
                'Failed asserting that closure declared at %s:%d is equal to closure declared at %s:%d.',
                $expectedFilename,
                $expectedStartLine,
                $actualFilename,
                $actualStartLine,
            ),
            $this->contextLines(),
        );
    }

    private function declarationIsEqual(ReflectionFunction $expected, ReflectionFunction $actual): bool
    {
        return $expected->getName() === $actual->getName() &&

View on GitHub (pinned to 00837a9d22)