sebastian/comparator
sebastian/comparator compares PHP values for equality with type-aware comparators. Use the Factory to select the right comparator and get helpful ComparisonFailure details when assertions fail—ideal for test suites and tooling.
Install the package as a dev dependency:
composer require --dev sebastianbergmann/comparator:^8.2
Basic usage in tests (PHPUnit/Pest):
use SebastianBergmann\Comparator\Factory;
use SebastianBergmann\Comparator\ComparisonFailure;
$factory = new Factory();
$comparator = $factory->getComparatorFor($actual, $expected);
try {
$comparator->assertEquals($actual, $expected);
// Pass
} catch (ComparisonFailure $e) {
// Fail with detailed diff (now cleaner for canonicalized lists)
$this->fail($e->getMessage());
}
Laravel-specific quick win: Replace assertSame() for Eloquent models/collections:
$user = User::find(1);
$expected = ['id' => 1, 'name' => 'John'];
$comparator->assertEquals($user->toArray(), $expected);
use Illuminate\Testing\TestResponse;
public function test_api_response()
{
$response = $this->getJson('/api/users');
$actual = $response->json();
$expected = ['data' => [['id' => 1, 'name' => 'John']]];
$factory = new Factory();
$comparator = $factory->getComparatorFor($actual, $expected, Factory::CANONICALIZE);
$comparator->assertEquals($actual, $expected);
// Note: Diff output is now cleaner for canonicalized lists (e.g., sorted arrays)
}
Leverage the Factory to automatically select the right comparator for your data type:
$factory = new Factory();
$comparator = $factory->getComparatorFor($actual, $expected);
// Handles DateTime, arrays, objects, and now **closures by value** (v8.2.1)
For order-agnostic testing (e.g., API responses, Eloquent collections):
$comparator = $factory->getComparatorFor($actual, $expected, Factory::CANONICALIZE);
// Fixed in v8.2.1: String keys are now preserved during canonicalization
Compare closures by value (previously compared by reference):
$closure1 = fn() => 'test';
$closure2 = fn() => 'test';
$comparator = $factory->getComparatorFor($closure1, $closure2);
$comparator->assertEquals($closure1, $closure2); // Now works!
Configure diff context lines for cleaner test failures:
$factory = new Factory();
$factory->setDiffContextLines(3); // Default is 3
// Note: Canonicalized list diffs are now more readable (v8.2.1)
public function test_user_creation()
{
$user = User::create(['name' => 'John']);
$expected = ['id' => 1, 'name' => 'John'];
$comparator = (new Factory())->getComparatorFor($user->toArray(), $expected, Factory::CANONICALIZE);
$comparator->assertEquals($user->toArray(), $expected);
// String keys in arrays are preserved during canonicalization (v8.2.1)
}
public function test_paginated_response()
{
$response = $this->getJson('/api/users?page=1');
$actual = $response->json()['data'];
$comparator = (new Factory())->getComparatorFor($actual, $expected, Factory::CANONICALIZE);
$comparator->assertEquals($actual, $expected);
// Diff output is now cleaner for sorted/unsorted array comparisons
}
public function test_seeder_output()
{
Artisan::call('db:seed');
$actual = DB::table('users')->get()->toArray();
$expected = [['id' => 1, 'name' => 'Admin']];
$comparator = (new Factory())->getComparatorFor($actual, $expected, Factory::CANONICALIZE);
$comparator->assertEquals($actual, $expected);
}
public function test_scheduled_job_timezone()
{
$actual = now();
$expected = Carbon::parse('2023-01-01 12:00:00');
$comparator = (new Factory())->getComparatorFor($actual, $expected);
$comparator->assertEquals($actual, $expected);
}
public function test_closure_behavior()
{
$closure1 = fn($x) => $x * 2;
$closure2 = fn($x) => $x * 2;
$comparator = (new Factory())->getComparatorFor($closure1, $closure2);
$comparator->assertEquals($closure1, $closure2); // Now compares by value
}
String Keys in Canonicalized Arrays (Fixed in v8.2.1)
Closure Comparison (New in v8.2.1)
$closure1 = fn() => 'test';
$closure2 = fn() => 'test';
// These will now pass comparison
Non-Deterministic Sorting in Mixed Arrays
[1, 'a', new stdClass()]) may still sort unpredictably.Factory::NORMALIZE.Timezone-Sensitive DateTime Comparisons
DateTime objects may fail if timezones differ.$actual->setTimezone(new DateTimeZone('UTC'));
$expected->setTimezone(new DateTimeZone('UTC'));
Performance Overhead
Factory::NORMALIZE instead of CANONICALIZE for performance-critical paths.Inspect Diff Output (Improved in v8.2.1)
Use the getDiff() method to debug failures (now cleaner for canonicalized lists):
try {
$comparator->assertEquals($actual, $expected);
} catch (ComparisonFailure $e) {
$this->fail($e->getDiff());
// Diffs for canonicalized arrays are now more readable
}
Log Comparison Details For complex objects, log the actual/expected values before comparison:
$this->info('Actual:', $actual);
$this->info('Expected:', $expected);
Test Canonicalization Separately Verify canonicalization works as expected (string keys are preserved):
$comparator = (new Factory())->getComparatorFor($actual, $expected, Factory::CANONICALIZE);
$this->assertTrue($comparator->compare($actual, $expected));
Handle Edge Cases Explicitly
For INF, -INF, or NaN:
if (is_infinite($actual) || is_nan($actual)) {
$this->fail("Actual value is INF/NaN");
}
Custom Comparators
Implement SebastianBergmann\Comparator\Comparator for Laravel-specific types:
class CollectionComparator implements Comparator
{
public function assertEquals($expected, $actual, $description = '', $delta = 0, $canonicalize = false, $ignoreCase = false)
{
if ($canonicalize) {
$actual = $actual->sortBy(fn($item) => $item['id'])->values();
$expected = Collection::make($expected)->sortBy(fn($item) => $item['id'])->values();
}
parent::assertEquals($expected, $actual, $description, $delta, $canonicalize, $ignoreCase);
}
}
Override Factory Behavior
Extend SebastianBergmann\Comparator\Factory to modify comparator selection:
How can I help you explore Laravel packages today?