sebastianbergmann/comparator · error · ComparisonFailure

is not instance of expected class " ".

Error message

%s is not instance of expected class "%s".

What it means

ObjectComparator, the base comparator for objects in sebastian/comparator, throws a ComparisonFailure when the actual object's class differs from the expected object's class. Subclass comparators (DateTime, SplObjectStorage, etc.) only accept same-class pairs, so cross-class comparisons land here and fail immediately.

Solutions

  1. Check the message and diff for the actual class name and fix the code to construct the expected class
  2. Update the expected object/class in the test if the new type is intended
  3. Normalize before comparing: cast/copy the value into the expected class or compare individual properties instead of whole objects
  4. If polymorphic equality is intended, register a custom comparator or compare with assertInstanceOf followed by property-level assertions

Example fix

// before
$this->assertEquals(new DateTime('2024-01-01'), $dto->date); // DateTimeImmutable vs DateTime
// after
$this->assertEquals(new DateTimeImmutable('2024-01-01'), $dto->date);
Defensive patterns

Strategy: type-guard

Validate before calling

if (get_class($actual) !== get_class($expected)) {
    throw new InvalidArgumentException(sprintf('Expected %s, got %s', get_class($expected), get_class($actual)));
}

Type guard

/** @phpstan-assert ExpectedClass $v */
function isExpectedClass(mixed $v): bool { return $v instanceof ExpectedClass && $v::class === ExpectedClass::class; }

Try / catch

try {
    $comparator->assertEquals($expectedObject, $actualObject);
} catch (ComparisonFailure $e) {
    // classes differ: fall back to property-level comparison or normalize types
}

Prevention

When it happens

Trigger: assertEquals($expectedObject, $actualObject) where $actual::class !== $expected::class, e.g. comparing a ParentClass instance to a ChildClass instance, or two different classes that happen to look similar (DateTime vs DateTimeImmutable, stdClass vs a DTO).

Common situations: Tests after a refactor replaced a DTO with a subclass or a differently-named class; mocking where a mock's class (e.g. created by PHPUnit's mock engine) differs from the real class; comparing DateTime with DateTimeImmutable; deserializers returning stdClass instead of the typed class.

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/5deb4fa3153a9b62. Report an issue: GitHub.

Appendix: source

Thrown at src/ObjectComparator.php:51

    {
        return is_object($expected) && is_object($actual);
    }

    /**
     * @param array<mixed> $processed
     *
     * @throws ComparisonFailure
     * @throws ObjectNotSupportedException
     */
    public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false, array &$processed = []): void
    {
        assert(is_object($expected));
        assert(is_object($actual));

        if ($actual::class !== $expected::class) {
            $exporter = $this->exporter();

            throw new ComparisonFailure(
                $expected,
                $actual,
                $exporter->export($expected),
                $exporter->export($actual),
                sprintf(
                    '%s is not instance of expected class "%s".',
                    $exporter->export($actual),
                    $expected::class,
                ),
                $this->contextLines(),
            );
        }

        // don't compare twice to allow for cyclic dependencies
        if (in_array([$actual, $expected], $processed, true) ||
            in_array([$expected, $actual], $processed, true)) {
            return;
        }

View on GitHub (pinned to 00837a9d22)