From aa615d5b926c4b1c9a8fed33dec0a44a68e8cbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Pelhate?= Date: Thu, 21 Aug 2025 14:31:16 +0200 Subject: [PATCH 1/3] Add ModelComparator to allow comparing Eloquent models in PHPUnit --- .../Constraint/Eloquent/ModelComparator.php | 38 +++++ .../Eloquent/ModelComparatorTest.php | 131 ++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 src/Laravel/Constraint/Eloquent/ModelComparator.php create mode 100644 src/Laravel/Constraint/Eloquent/ModelComparatorTest.php diff --git a/src/Laravel/Constraint/Eloquent/ModelComparator.php b/src/Laravel/Constraint/Eloquent/ModelComparator.php new file mode 100644 index 0000000..7ccd460 --- /dev/null +++ b/src/Laravel/Constraint/Eloquent/ModelComparator.php @@ -0,0 +1,38 @@ +is($expected) or throw new ComparisonFailure( + $expected, + $actual, + $expected->getKey(), + $actual->getKey(), + 'Failed asserting that two Eloquent models are equal.', + ); + } +} diff --git a/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php new file mode 100644 index 0000000..a512834 --- /dev/null +++ b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php @@ -0,0 +1,131 @@ + [ + 'not-a-model', + 'not-a-model', + false, + ]; + + yield 'Expected is not a model' => [ + new stdClass(), + self::model('model', 'default'), + false, + ]; + + yield 'Actual is not a model' => [ + self::model('model', 'default'), + self::model('model', 'default'), + true, + ]; + } + + #[Test] + #[DataProvider('accepts')] + public function itShouldOnlyAcceptExpectedAndActualModels(mixed $expected, mixed $actual, bool $accepted): void + { + $instance = new ModelComparator(); + + $result = $instance->accepts($expected, $actual); + + $this->assertSame($accepted, $result); + } + + public static function notEqual(): iterable + { + yield 'Expected nor actual are models' => [ + 'not-a-model', + 'not-a-model', + AssertionError::class, + ]; + + yield 'Expected is not a model' => [ + new stdClass(), + self::model('model', 'default'), + AssertionError::class, + ]; + + yield 'Expected and actual have different IDs' => [ + self::model('model', 'default', Str::uuid()->toString()), + self::model('model', 'default', Str::uuid()->toString()), + ComparisonFailure::class, + ]; + + yield 'Expected and actual have different tables' => [ + $expected = self::model('model1', 'default'), + self::model('model2', 'default', $expected->getKey()), + ComparisonFailure::class, + ]; + + yield 'Expected and actual have different tables' => [ + $expected = self::model('model', 'default1'), + self::model('model', 'default2', $expected->getKey()), + ComparisonFailure::class, + ]; + } + + #[Test] + #[DataProvider('notEqual')] + public function itShouldFailWhenNotEqual(mixed $expected, mixed $actual, string $exceptionFQCN): void + { + $instance = new ModelComparator(); + + $this->expectException($exceptionFQCN); + + $instance->assertEquals($expected, $actual); + } + + public static function isEqual(): iterable + { + yield 'Expected and actual are same instance' => [ + $model = self::model('model', 'default'), + $model, + ]; + + yield 'Expected and actual are different instances' => [ + $model = self::model('model', 'default'), + self::model('model', 'default', $model->getKey()), + ]; + } + + #[Test] + #[DataProvider('isEqual')] + public function itShouldPassWhenEqual(Model $expected, Model $actual): void + { + $instance = new ModelComparator(); + + $this->expectNotToPerformAssertions(); + + $instance->assertEquals($expected, $actual); + } + + private static function model(string $table, string $connection, string $id = ''): Model + { + $id ??= new Randomizer()->getInt(1, 10000000000); + $model = new class(compact('id')) extends Model { + protected $fillable = ['id']; + protected $keyType = 'string'; + }; + + return $model->setConnection($connection)->setTable($table); + } +} From c0ee11e0cf6f0220c739f5fd832553e6355db038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Pelhate?= Date: Thu, 21 Aug 2025 14:34:44 +0200 Subject: [PATCH 2/3] Fix data provider --- src/Laravel/Constraint/Eloquent/ModelComparatorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php index a512834..ae322b8 100644 --- a/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php +++ b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php @@ -76,7 +76,7 @@ public static function notEqual(): iterable ComparisonFailure::class, ]; - yield 'Expected and actual have different tables' => [ + yield 'Expected and actual have different connections' => [ $expected = self::model('model', 'default1'), self::model('model', 'default2', $expected->getKey()), ComparisonFailure::class, From 1110d7977a05ad5455841a7fb8bc5cfcbac94a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Pelhate?= Date: Thu, 21 Aug 2025 14:42:34 +0200 Subject: [PATCH 3/3] =?UTF-8?q?Don=E2=80=99t=20rely=20on=20assert=20except?= =?UTF-8?q?ions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assert.exception option may not be enabled for projects using this package. --- .../Constraint/Eloquent/ModelComparator.php | 14 ++++++++++---- .../Constraint/Eloquent/ModelComparatorTest.php | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Laravel/Constraint/Eloquent/ModelComparator.php b/src/Laravel/Constraint/Eloquent/ModelComparator.php index 7ccd460..c98c07f 100644 --- a/src/Laravel/Constraint/Eloquent/ModelComparator.php +++ b/src/Laravel/Constraint/Eloquent/ModelComparator.php @@ -4,12 +4,11 @@ namespace Craftzing\TestBench\Laravel\Constraint\Eloquent; +use AssertionError; use Illuminate\Database\Eloquent\Model; use SebastianBergmann\Comparator\Comparator; use SebastianBergmann\Comparator\ComparisonFailure; -use function assert; - final class ModelComparator extends Comparator { public function accepts(mixed $expected, mixed $actual): bool @@ -24,8 +23,8 @@ public function assertEquals( bool $canonicalize = false, bool $ignoreCase = false, ): void { - assert($expected instanceof Model); - assert($actual instanceof Model); + $expected instanceof Model or throw self::notInstanceOfModel('expected', $expected); + $actual instanceof Model or throw self::notInstanceOfModel('actual', $actual); $actual->is($expected) or throw new ComparisonFailure( $expected, @@ -35,4 +34,11 @@ public function assertEquals( 'Failed asserting that two Eloquent models are equal.', ); } + + private static function notInstanceOfModel(string $argumentName, mixed $value): AssertionError + { + return new AssertionError( + "Argument $argumentName must be an instance of " . Model::class . ', received ' . gettype($value) . '.', + ); + } } diff --git a/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php index ae322b8..f426a5a 100644 --- a/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php +++ b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php @@ -41,7 +41,7 @@ public static function accepts(): iterable #[Test] #[DataProvider('accepts')] - public function itShouldOnlyAcceptExpectedAndActualModels(mixed $expected, mixed $actual, bool $accepted): void + public function itOnlyAcceptExpectedAndActualModels(mixed $expected, mixed $actual, bool $accepted): void { $instance = new ModelComparator(); @@ -85,7 +85,7 @@ public static function notEqual(): iterable #[Test] #[DataProvider('notEqual')] - public function itShouldFailWhenNotEqual(mixed $expected, mixed $actual, string $exceptionFQCN): void + public function itFailsWhenNotEqual(mixed $expected, mixed $actual, string $exceptionFQCN): void { $instance = new ModelComparator(); @@ -109,7 +109,7 @@ public static function isEqual(): iterable #[Test] #[DataProvider('isEqual')] - public function itShouldPassWhenEqual(Model $expected, Model $actual): void + public function itPassesWhenEqual(Model $expected, Model $actual): void { $instance = new ModelComparator();