$value) { $object->$key = $value; } return $object; } private function createComplexObject(array $data): TestObject { return new TestObject($data); } // EQ Operator Tests public function testWhenUsingEqWithStringThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::eq('name', self::TEST_STRING)); $matchingObject = $this->createSimpleObject(['name' => self::TEST_STRING]); $nonMatchingObject = $this->createSimpleObject(['name' => 'different']); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } public function testWhenUsingEqWithIntegerThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::eq('age', self::TEST_INT)); $matchingObject = $this->createSimpleObject(['age' => self::TEST_INT]); $nonMatchingObject = $this->createSimpleObject(['age' => 100]); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } public function testWhenUsingEqWithFloatThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::eq('price', self::TEST_FLOAT)); $matchingObject = $this->createSimpleObject(['price' => self::TEST_FLOAT]); $nonMatchingObject = $this->createSimpleObject(['price' => 1.0]); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } public function testWhenUsingEqWithComplexObjectGetterThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::eq('name', self::TEST_STRING)); $matchingObject = $this->createComplexObject(['name' => self::TEST_STRING]); $nonMatchingObject = $this->createComplexObject(['name' => 'different']); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } // NEQ Operator Tests public function testWhenUsingNeqThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::neq('status', 'inactive')); $matchingObject = $this->createSimpleObject(['status' => 'active']); $nonMatchingObject = $this->createSimpleObject(['status' => 'inactive']); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } // Comparison Operators Tests public function testWhenUsingGtThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::gt('score', 80)); $matchingObject = $this->createSimpleObject(['score' => 90]); $nonMatchingObject = $this->createSimpleObject(['score' => 70]); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } public function testWhenUsingGteThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::gte('score', 80)); $matchingObject1 = $this->createSimpleObject(['score' => 80]); $matchingObject2 = $this->createSimpleObject(['score' => 90]); $nonMatchingObject = $this->createSimpleObject(['score' => 70]); // When $matchResult1 = $spec->match($matchingObject1); $matchResult2 = $spec->match($matchingObject2); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult1); $this->assertTrue($matchResult2); $this->assertFalse($noMatchResult); } public function testWhenUsingLtThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::lt('score', 80)); $matchingObject = $this->createSimpleObject(['score' => 70]); $nonMatchingObject = $this->createSimpleObject(['score' => 90]); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } public function testWhenUsingLteThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::lte('score', 80)); $matchingObject1 = $this->createSimpleObject(['score' => 80]); $matchingObject2 = $this->createSimpleObject(['score' => 70]); $nonMatchingObject = $this->createSimpleObject(['score' => 90]); // When $matchResult1 = $spec->match($matchingObject1); $matchResult2 = $spec->match($matchingObject2); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult1); $this->assertTrue($matchResult2); $this->assertFalse($noMatchResult); } // IN Operator Tests public function testWhenUsingInThenMatchesCorrectly(): void { // Given $validValues = ['admin', 'moderator', 'editor']; $spec = new Specification(Spec::in('role', $validValues)); $matchingObject = $this->createSimpleObject(['role' => 'admin']); $nonMatchingObject = $this->createSimpleObject(['role' => 'user']); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } public function testWhenUsingInWithEmptyArrayThenNeverMatches(): void { // Given $spec = new Specification(Spec::in('role', [])); $testObject = $this->createSimpleObject(['role' => 'admin']); // When $result = $spec->match($testObject); // Then $this->assertFalse($result); } // NOT IN Operator Tests public function testWhenUsingNotInThenMatchesCorrectly(): void { // Given $invalidValues = ['banned', 'suspended']; $spec = new Specification(Spec::nin('status', $invalidValues)); $matchingObject = $this->createSimpleObject(['status' => 'active']); $nonMatchingObject = $this->createSimpleObject(['status' => 'banned']); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } // DateTime Tests public function testWhenUsingDateTimeComparisonWithStringsThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::lt('createdAt', self::TEST_DATE_2)); $matchingObject = $this->createSimpleObject(['createdAt' => self::TEST_DATE]); $nonMatchingObject = $this->createSimpleObject(['createdAt' => self::TEST_DATE_2]); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } public function testWhenUsingDateTimeComparisonWithDateTimeObjectsThenMatchesCorrectly(): void { // Given $testDate = new DateTimeImmutable(self::TEST_DATE); $spec = new Specification(Spec::lte('expirationDate', $testDate)); $matchingObject = $this->createComplexObject(['expirationDate' => new DateTimeImmutable(self::TEST_DATE)]); $nonMatchingObject = $this->createComplexObject(['expirationDate' => new DateTimeImmutable(self::TEST_DATE_2)]); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } public function testWhenUsingDateTimeComparisonWithMixedTypesThenMatchesCorrectly(): void { // Given $testDate = new DateTimeImmutable(self::TEST_DATE); $spec = new Specification(Spec::eq('createdAt', $testDate)); $matchingObject = $this->createSimpleObject(['createdAt' => self::TEST_DATE]); // When $result = $spec->match($matchingObject); // Then $this->assertTrue($result); } // AND Group Tests public function testWhenUsingAndGroupThenMatchesOnlyWhenAllConditionsMet(): void { // Given $spec = new Specification(Spec::and([ Spec::eq('status', 'active'), Spec::gte('score', 80), Spec::in('role', ['admin', 'moderator']) ])); $matchingObject = $this->createSimpleObject([ 'status' => 'active', 'score' => 85, 'role' => 'admin' ]); $nonMatchingObject1 = $this->createSimpleObject([ 'status' => 'inactive', 'score' => 85, 'role' => 'admin' ]); $nonMatchingObject2 = $this->createSimpleObject([ 'status' => 'active', 'score' => 70, 'role' => 'admin' ]); $nonMatchingObject3 = $this->createSimpleObject([ 'status' => 'active', 'score' => 85, 'role' => 'user' ]); // When $matchResult = $spec->match($matchingObject); $noMatchResult1 = $spec->match($nonMatchingObject1); $noMatchResult2 = $spec->match($nonMatchingObject2); $noMatchResult3 = $spec->match($nonMatchingObject3); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult1); $this->assertFalse($noMatchResult2); $this->assertFalse($noMatchResult3); } public function testWhenUsingEmptyAndGroupThenAlwaysMatches(): void { // Given $spec = new Specification(Spec::and([])); $testObject = $this->createSimpleObject(['any' => 'value']); // When $result = $spec->match($testObject); // Then $this->assertTrue($result); } // OR Group Tests public function testWhenUsingOrGroupThenMatchesWhenAnyConditionMet(): void { // Given $spec = new Specification(Spec::or([ Spec::eq('role', 'admin'), Spec::gte('score', 90), Spec::in('department', ['IT', 'HR']) ])); $matchingObject1 = $this->createSimpleObject([ 'role' => 'admin', 'score' => 70, 'department' => 'Finance' ]); $matchingObject2 = $this->createSimpleObject([ 'role' => 'user', 'score' => 95, 'department' => 'Finance' ]); $matchingObject3 = $this->createSimpleObject([ 'role' => 'user', 'score' => 70, 'department' => 'IT' ]); $nonMatchingObject = $this->createSimpleObject([ 'role' => 'user', 'score' => 70, 'department' => 'Finance' ]); // When $matchResult1 = $spec->match($matchingObject1); $matchResult2 = $spec->match($matchingObject2); $matchResult3 = $spec->match($matchingObject3); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult1); $this->assertTrue($matchResult2); $this->assertTrue($matchResult3); $this->assertFalse($noMatchResult); } public function testWhenUsingEmptyOrGroupThenNeverMatches(): void { // Given $spec = new Specification(Spec::or([])); $testObject = $this->createSimpleObject(['any' => 'value']); // When $result = $spec->match($testObject); // Then $this->assertFalse($result); } // NOT Group Tests public function testWhenUsingNotGroupThenInvertsCondition(): void { // Given $spec = new Specification(Spec::not(Spec::eq('status', 'banned'))); $matchingObject = $this->createSimpleObject(['status' => 'active']); $nonMatchingObject = $this->createSimpleObject(['status' => 'banned']); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } // Complex Nested Groups Tests public function testWhenUsingNestedAndOrGroupsThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::and([ Spec::eq('status', 'active'), Spec::or([ Spec::gte('score', 80), Spec::in('role', ['admin', 'moderator']) ]) ])); $matchingObject1 = $this->createSimpleObject([ 'status' => 'active', 'score' => 85, 'role' => 'user' ]); $matchingObject2 = $this->createSimpleObject([ 'status' => 'active', 'score' => 70, 'role' => 'admin' ]); $nonMatchingObject = $this->createSimpleObject([ 'status' => 'inactive', 'score' => 85, 'role' => 'user' ]); // When $matchResult1 = $spec->match($matchingObject1); $matchResult2 = $spec->match($matchingObject2); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult1); $this->assertTrue($matchResult2); $this->assertFalse($noMatchResult); } public function testWhenUsingTripleNestedGroupsThenMatchesCorrectly(): void { // Given $spec = new Specification(Spec::and([ Spec::eq('active', true), Spec::not(Spec::or([ Spec::eq('role', 'banned'), Spec::eq('status', 'suspended') ])) ])); $matchingObject = $this->createSimpleObject([ 'active' => true, 'role' => 'user', 'status' => 'active' ]); $nonMatchingObject1 = $this->createSimpleObject([ 'active' => false, 'role' => 'user', 'status' => 'active' ]); $nonMatchingObject2 = $this->createSimpleObject([ 'active' => true, 'role' => 'banned', 'status' => 'active' ]); // When $matchResult = $spec->match($matchingObject); $noMatchResult1 = $spec->match($nonMatchingObject1); $noMatchResult2 = $spec->match($nonMatchingObject2); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult1); $this->assertFalse($noMatchResult2); } // Edge Case Tests public function testWhenFieldDoesNotExistThenReturnsFalse(): void { // Given $spec = new Specification(Spec::eq('nonExistentField', 'value')); $testObject = $this->createSimpleObject(['existingField' => 'value']); // When $result = $spec->match($testObject); // Then $this->assertFalse($result); } public function testWhenFieldIsNullThenComparesCorrectly(): void { // Given $spec = new Specification(Spec::eq('optionalField', null)); $matchingObject = $this->createSimpleObject(['optionalField' => null]); $nonMatchingObject = $this->createSimpleObject(['optionalField' => 'value']); // When $matchResult = $spec->match($matchingObject); $noMatchResult = $spec->match($nonMatchingObject); // Then $this->assertTrue($matchResult); $this->assertFalse($noMatchResult); } public function testWhenUsingInvalidOperatorThenReturnsFalse(): void { // Given $spec = new Specification(['field', 'INVALID_OP', 'value']); $testObject = $this->createSimpleObject(['field' => 'value']); // When $result = $spec->match($testObject); // Then $this->assertFalse($result); } public function testWhenUsingInvalidDateStringThenFallsBackToRegularComparison(): void { // Given $spec = new Specification(Spec::eq('dateField', 'invalid-date')); $testObject = $this->createSimpleObject(['dateField' => 'invalid-date']); // When $result = $spec->match($testObject); // Then $this->assertTrue($result); } // Spec Helper Method Tests public function testWhenUsingSpecHelpersThenCreatesCorrectSpecificationArray(): void { // Given $eqSpec = Spec::eq('field', 'value'); $neqSpec = Spec::neq('field', 'value'); $gtSpec = Spec::gt('field', 10); $gteSpec = Spec::gte('field', 10); $ltSpec = Spec::lt('field', 10); $lteSpec = Spec::lte('field', 10); $inSpec = Spec::in('field', ['a', 'b']); $ninSpec = Spec::nin('field', ['a', 'b']); // When & Then $this->assertEquals(['field', '=', 'value'], $eqSpec); $this->assertEquals(['field', '!=', 'value'], $neqSpec); $this->assertEquals(['field', '>', 10], $gtSpec); $this->assertEquals(['field', '>=', 10], $gteSpec); $this->assertEquals(['field', '<', 10], $ltSpec); $this->assertEquals(['field', '<=', 10], $lteSpec); $this->assertEquals(['field', 'IN', ['a', 'b']], $inSpec); $this->assertEquals(['field', 'NOT IN', ['a', 'b']], $ninSpec); } public function testWhenUsingLogicalGroupHelpersThenCreatesCorrectSpecificationArray(): void { // Given $andSpec = Spec::and([Spec::eq('a', 1), Spec::eq('b', 2)]); $orSpec = Spec::or([Spec::eq('a', 1), Spec::eq('b', 2)]); $notSpec = Spec::not(Spec::eq('a', 1)); // When & Then $this->assertEquals(['AND' => [['a', '=', 1], ['b', '=', 2]]], $andSpec); $this->assertEquals(['OR' => [['a', '=', 1], ['b', '=', 2]]], $orSpec); $this->assertEquals(['NOT' => ['a', '=', 1]], $notSpec); } public function testGetSpecReturnsOriginalSpecification(): void { // Given $originalSpec = Spec::eq('field', 'value'); $specification = new Specification($originalSpec); // When $retrievedSpec = $specification->getSpec(); // Then $this->assertEquals($originalSpec, $retrievedSpec); } } // Test helper class with getters for complex object testing class TestObject { private array $data; public function __construct(array $data) { $this->data = $data; } public function __get($name) { return $this->data[$name] ?? null; } public function __isset($name) { return isset($this->data[$name]); } public function getName() { return $this->data['name'] ?? null; } public function getExpirationDate() { return $this->data['expirationDate'] ?? null; } public function getCreatedAt() { return $this->data['createdAt'] ?? null; } public function getScore() { return $this->data['score'] ?? null; } public function getStatus() { return $this->data['status'] ?? null; } public function getRole() { return $this->data['role'] ?? null; } public function getAge() { return $this->data['age'] ?? null; } public function getPrice() { return $this->data['price'] ?? null; } public function getDepartment() { return $this->data['department'] ?? null; } public function getActive() { return $this->data['active'] ?? null; } public function getOptionalField() { return $this->data['optionalField'] ?? null; } public function getDateField() { return $this->data['dateField'] ?? null; } }