Multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

688 lines
21 KiB

<?php
declare(strict_types=1);
namespace AutoStore\Tests\Unit\Domain\Specifications;
use AutoStore\Domain\Specifications\Specification;
use AutoStore\Domain\Specifications\Spec;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use stdClass;
class SpecificationTest extends TestCase
{
// Test data constants
private const TEST_STRING = 'test-value';
private const TEST_INT = 42;
private const TEST_FLOAT = 3.14;
private const TEST_DATE = '2023-01-15 10:30:00';
private const TEST_DATE_2 = '2023-02-15 10:30:00';
private function createSimpleObject(array $data): stdClass
{
$object = new stdClass();
foreach ($data as $key => $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;
}
}