feat: ✨ add unit tests for Coupon entity and associated value objects

This commit is contained in:
Corentin Huet 2026-09-22 12:45:08 +02:00
parent 5767011375
commit dbcaa60dce
17 changed files with 751 additions and 3 deletions

View file

@ -16,5 +16,10 @@ final readonly class CouponProduct
/** @var CouponProductInfo[] */
public array $labels,
) {
$trimmed = trim($this->name);
if ($trimmed === '') {
throw new \DomainException('Coupon Product name cannot be empty.');
}
}
}

View file

@ -15,11 +15,23 @@ final readonly class CouponProductInfo implements Equatable
public string $validityText,
public Country $forLocale
) {
$trimmed = trim($this->name);
$trimmedName = trim($this->name);
if ($trimmed === '') {
if ($trimmedName === '') {
throw new \DomainException('Product Info name cannot be empty.');
}
$trimmedRange = trim($this->range);
if ($trimmedRange === '') {
throw new \DomainException('Product Info range cannot be empty.');
}
$trimmedValidityText = trim($this->validityText);
if ($trimmedValidityText === '') {
throw new \DomainException('Product Info validity text cannot be empty.');
}
}
#[\Override]

View file

@ -14,7 +14,7 @@ final readonly class ProcessingCode implements \Stringable, Equatable
$trimmed = trim($this->value);
if ($trimmed === '') {
throw new \DomainException('GS1 value cannot be empty.');
throw new \DomainException('Processing Code value cannot be empty.');
}
}

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Shared\Enums;
use Domain\Enums\BarcodeFormat;
covers(BarcodeFormat::class);
describe('barcode format', function (): void {
it('has the expected cases with their backing values', function (): void {
// THEN
expect(BarcodeFormat::EAN13->value())->toBe(1)
->and(BarcodeFormat::CODE128->value())->toBe(2)
->and(BarcodeFormat::GS1_128->value())->toBe(3);
});
it('exposes its case name', function (): void {
// THEN
expect(BarcodeFormat::EAN13->name())->toBe('EAN13');
});
it('returns the defined label', function (): void {
// THEN
expect(BarcodeFormat::EAN13->label())->toBe('EAN')
->and(BarcodeFormat::CODE128->label())->toBe('CODE 128')
->and(BarcodeFormat::GS1_128->label())->toBe('GS1_128');
});
it('lists all labels keyed by case name', function (): void {
// THEN
expect(BarcodeFormat::toLabels())->toBe([
'EAN13' => 'EAN',
'CODE128' => 'CODE 128',
'GS1_128' => 'GS1_128',
]);
});
it('lists all values keyed by case name', function (): void {
// THEN
expect(BarcodeFormat::toValues())->toBe([
'EAN13' => 1,
'CODE128' => 2,
'GS1_128' => 3,
]);
});
it('makes an instance from its backing value', function (): void {
// THEN
expect(BarcodeFormat::make(1))->toBe(BarcodeFormat::EAN13);
});
it('makes an instance from a label', function (): void {
// THEN
expect(BarcodeFormat::make('CODE 128', 'label'))->toBe(BarcodeFormat::CODE128);
});
it('returns the same instance when already an enum case', function (): void {
// THEN
expect(BarcodeFormat::make(BarcodeFormat::GS1_128))->toBe(BarcodeFormat::GS1_128);
});
it('returns null for an unknown value', function (): void {
// THEN
expect(BarcodeFormat::make(99))->toBeNull();
});
it('implements equals correctly', function (): void {
// THEN
expect(BarcodeFormat::EAN13->equals(BarcodeFormat::EAN13))->toBeTrue()
->and(BarcodeFormat::EAN13->equals(BarcodeFormat::CODE128))->toBeFalse()
->and(BarcodeFormat::EAN13->equals(null))->toBeFalse();
});
it('serializes to json as its backing value', function (): void {
// THEN
expect(BarcodeFormat::EAN13->jsonSerialize())->toBe(1)
->and(json_encode(BarcodeFormat::EAN13))->toBe('1');
});
});

View file

@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\Entities;
use Carbon\CarbonImmutable;
use Domain\Enums\BarcodeFormat;
use Domain\Enums\Country;
use Domain\Models\Entities\Coupon;
use Domain\Models\ValueObjects\Brand\BrandCode;
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
use Domain\Models\ValueObjects\Coupon\CouponId;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
use Domain\Models\ValueObjects\Coupon\CouponProduct;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use Domain\Models\ValueObjects\CouponName;
use Domain\Models\ValueObjects\CouponPicture;
use Domain\Models\ValueObjects\CouponProductName;
use Domain\Models\ValueObjects\GS1;
use Domain\Models\ValueObjects\MoneyAmount;
use Domain\Models\ValueObjects\PicturePath;
use Domain\Models\ValueObjects\ProcessingCode;
use Ramsey\Uuid\Uuid;
use Shared\Enums\Currency;
use Shared\ValueObjects\DateTime;
use Shared\ValueObjects\Money;
covers(Coupon::class);
describe('coupon', function (): void {
afterEach(function (): void {
CarbonImmutable::setTestNow();
});
it('creates a coupon', function (): void {
// GIVEN
$id = CouponId::generate();
$name = new CouponName('Name');
$status = \boolval(0);
$productName = new CouponProductName('Lorem ipsum');
$amount = new MoneyAmount(new Money(Currency::EUR, 1));
$picture = new CouponPicture(new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg'));
$product = new CouponProduct('Lorem ipsum', [], []);
$diffusion = new CouponDiffusion(['2026-08-25', '2026-10-31']);
$availability = new CouponAvailabilityCalendar(['2026-08-25', '2026-10-31']);
$validity = new CouponValidity(30, new DateTime('2026-11-30'));
$legal = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR);
$gtin = new GS1('110101010', BarcodeFormat::EAN13);
$code = new ProcessingCode('5425003944130');
$brandCode = new BrandCode('AAA01');
// WHEN
$coupon = Coupon::hydrate(
$id,
$name,
$status,
$productName,
$amount,
$picture,
$product,
$diffusion,
$availability,
$validity,
$legal,
$gtin,
$code,
$brandCode
);
// THEN
expect($coupon->id())->toBe($id)
->and($coupon->name)->toBe($name)
->and($coupon->status)->toBe($status)
->and($coupon->productName)->toBe($productName)
->and($coupon->amount)->toBe($amount)
->and($coupon->picture)->toBe($picture)
->and($coupon->product)->toBe($product)
->and($coupon->diffusion)->toBe($diffusion)
->and($coupon->availability)->toBe($availability)
->and($coupon->validity)->toBe($validity)
->and($coupon->legal)->toBe($legal)
->and($coupon->gtin)->toBe($gtin)
->and($coupon->code)->toBe($code)
->and($coupon->brandCode)->toBe($brandCode)
->and($coupon->updatedAt)->toBeNull()
->and($coupon->updatedAt())->toBeNull();
});
it('creates a coupon with an explicit updatedAt date', function (): void {
// GIVEN
$id = CouponId::generate();
$name = new CouponName('Name');
$status = \boolval(0);
$productName = new CouponProductName('Lorem ipsum');
$amount = new MoneyAmount(new Money(Currency::EUR, 1));
$picture = new CouponPicture(new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg'));
$product = new CouponProduct('Lorem ipsum', [], []);
$diffusion = new CouponDiffusion(['2026-08-25', '2026-10-31']);
$availability = new CouponAvailabilityCalendar(['2026-08-25', '2026-10-31']);
$validity = new CouponValidity(30, new DateTime('2026-11-30'));
$legal = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR);
$gtin = new GS1('110101010', BarcodeFormat::EAN13);
$code = new ProcessingCode('5425003944130');
$brandCode = new BrandCode('AAA01');
$updatedAt = new \DateTimeImmutable('2026-09-22 10:00:00', new \DateTimeZone('UTC'));
// WHEN
$coupon = Coupon::hydrate(
$id,
$name,
$status,
$productName,
$amount,
$picture,
$product,
$diffusion,
$availability,
$validity,
$legal,
$gtin,
$code,
$brandCode,
$updatedAt
);
// THEN
expect($coupon->updatedAt)->toBe($updatedAt)
->and($coupon->updatedAt())->toBe($updatedAt);
});
it('creates a new coupon with a freshly generated id', function (): void {
// GIVEN
$name = new CouponName('Name');
$status = \boolval(0);
$productName = new CouponProductName('Lorem ipsum');
$amount = new MoneyAmount(new Money(Currency::EUR, 1));
$picture = new CouponPicture(new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg'));
$product = new CouponProduct('Lorem ipsum', [], []);
$diffusion = new CouponDiffusion(['2026-08-25', '2026-10-31']);
$availability = new CouponAvailabilityCalendar(['2026-08-25', '2026-10-31']);
$validity = new CouponValidity(30, new DateTime('2026-11-30'));
$legal = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR);
$gtin = new GS1('110101010', BarcodeFormat::EAN13);
$code = new ProcessingCode('5425003944130');
$brandCode = new BrandCode('AAA01');
// WHEN
$coupon = Coupon::create(
$name,
$status,
$productName,
$amount,
$picture,
$product,
$diffusion,
$availability,
$validity,
$legal,
$gtin,
$code,
$brandCode
);
// THEN
expect($coupon->id())->toBeInstanceOf(CouponId::class)
->and($coupon->name)->toBe($name)
->and($coupon->status)->toBe($status)
->and($coupon->productName)->toBe($productName)
->and($coupon->amount)->toBe($amount)
->and($coupon->picture)->toBe($picture)
->and($coupon->product)->toBe($product)
->and($coupon->diffusion)->toBe($diffusion)
->and($coupon->availability)->toBe($availability)
->and($coupon->validity)->toBe($validity)
->and($coupon->legal)->toBe($legal)
->and($coupon->gtin)->toBe($gtin)
->and($coupon->code)->toBe($code)
->and($coupon->brandCode)->toBe($brandCode)
->and($coupon->updatedAt)->toBeNull()
->and($coupon->updatedAt())->toBeNull();
});
it('generates a different id for each created coupon', function (): void {
// GIVEN
$name = new CouponName('Name');
$status = \boolval(0);
$productName = new CouponProductName('Lorem ipsum');
$amount = new MoneyAmount(new Money(Currency::EUR, 1));
$picture = new CouponPicture(new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg'));
$product = new CouponProduct('Lorem ipsum', [], []);
$diffusion = new CouponDiffusion(['2026-08-25', '2026-10-31']);
$availability = new CouponAvailabilityCalendar(['2026-08-25', '2026-10-31']);
$validity = new CouponValidity(30, new DateTime('2026-11-30'));
$legal = new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR);
$gtin = new GS1('110101010', BarcodeFormat::EAN13);
$code = new ProcessingCode('5425003944130');
$brandCode = new BrandCode('AAA01');
// WHEN
$firstCoupon = Coupon::create(
$name,
$status,
$productName,
$amount,
$picture,
$product,
$diffusion,
$availability,
$validity,
$legal,
$gtin,
$code,
$brandCode
);
$secondCoupon = Coupon::create(
$name,
$status,
$productName,
$amount,
$picture,
$product,
$diffusion,
$availability,
$validity,
$legal,
$gtin,
$code,
$brandCode
);
// THEN
expect($firstCoupon->id()->equals($secondCoupon->id()))->toBeFalse();
});
it('returns the creation date extracted from the id', function (): void {
// GIVEN
$expected = new \DateTimeImmutable('2024-06-15 12:34:56.789000', new \DateTimeZone('UTC'));
$id = new CouponId(Uuid::uuid7($expected));
$coupon = Coupon::hydrate(
$id,
new CouponName('Name'),
\boolval(0),
new CouponProductName('Lorem ipsum'),
new MoneyAmount(new Money(Currency::EUR, 1)),
new CouponPicture(new PicturePath('files/product.jpg'), new PicturePath('files/thumbnail.jpg')),
new CouponProduct('Lorem ipsum', [], []),
new CouponDiffusion(['2026-08-25', '2026-10-31']),
new CouponAvailabilityCalendar(['2026-08-25', '2026-10-31']),
new CouponValidity(30, new DateTime('2026-11-30')),
new CouponLegalInformation('Lorem ipsum dolor sit amet consectetur adipiscing elit.', Country::FR),
new GS1('110101010', BarcodeFormat::EAN13),
new ProcessingCode('5425003944130'),
new BrandCode('AAA01')
);
// WHEN
$result = $coupon->createdAt();
// THEN
expect($result->format('Y-m-d H:i:s.v'))->toBe($expected->format('Y-m-d H:i:s.v'));
});
});

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponAvailabilityCalendar;
covers(CouponAvailabilityCalendar::class);
describe('coupon availability calendar', function (): void {
it('creates a valid coupon availability calendar', function (): void {
// GIVEN
$periods = ['2026-08-25', '2026-10-31'];
// WHEN
$couponAvailabilityCalendar = new CouponAvailabilityCalendar($periods);
// THEN
expect($couponAvailabilityCalendar->periods)->toBe($periods);
});
it('rejects invalid coupon availability calendar', function (): void {
// GIVEN
$periods = [];
// GIVEN / THEN
expect(fn (): CouponAvailabilityCalendar => new CouponAvailabilityCalendar($periods))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponDiffusion;
covers(CouponDiffusion::class);
describe('coupon diffusion', function (): void {
it('creates a valid coupon diffusion', function (): void {
// GIVEN
$periods = ['2026-08-25', '2026-10-31'];
// WHEN
$couponDiffusion = new CouponDiffusion($periods);
// THEN
expect($couponDiffusion->periods)->toBe($periods);
});
it('rejects invalid coupon diffusion', function (): void {
// GIVEN
$periods = [];
// GIVEN / THEN
expect(fn (): CouponDiffusion => new CouponDiffusion($periods))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponId;
covers(CouponId::class);
describe('coupon id', function (): void {
it('generates a unique coupon id', function (): void {
// WHEN
$id = CouponId::generate();
// THEN
expect($id)->toBeInstanceOf(CouponId::class);
});
it('generates different ids on each call', function (): void {
// WHEN
$first = CouponId::generate();
$second = CouponId::generate();
// THEN
expect((string) $first)->not->toBe((string) $second);
});
});

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Enums\Country;
use Domain\Models\ValueObjects\Coupon\CouponLegalInformation;
covers(CouponLegalInformation::class);
describe('coupon legal information', function (): void {
it('creates a valid coupon legal information', function (): void {
// GIVEN
$legalText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.';
$forLocale = Country::FR;
// WHEN
$couponLegalInformation = new CouponLegalInformation($legalText, $forLocale);
// THEN
expect($couponLegalInformation->legalText)->toBe($legalText)
->and($couponLegalInformation->forLocale)->toBe($forLocale);
});
it('rejects invalid coupon legal information', function (): void {
// GIVEN
$legalText = '';
$forLocale = Country::FR;
// GIVEN / THEN
expect(fn (): CouponLegalInformation => new CouponLegalInformation($legalText, $forLocale))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Enums\Country;
use Domain\Models\ValueObjects\Coupon\CouponProductInfo;
covers(CouponProductInfo::class);
describe('coupon product info', function (): void {
it('creates a valid coupon product info', function (): void {
// GIVEN
$name = 'Lorem ipsum';
$range = 'Lorem';
$validityText = 'Lorem ipsum dolor sit amet consectetur adipiscing elit.';
$forLocale = Country::FR;
// WHEN
$couponProductInfo = new CouponProductInfo($name, $range, $validityText, $forLocale);
// THEN
expect($couponProductInfo->name)->toBe($name)
->and($couponProductInfo->range)->toBe($range)
->and($couponProductInfo->validityText)->toBe($validityText)
->and($couponProductInfo->forLocale)->toBe($forLocale);
});
it('rejects invalid coupon product info', function (): void {
// GIVEN
$name = '';
$range = '';
$validityText = '';
$forLocale = Country::FR;
// GIVEN / THEN
expect(fn (): CouponProductInfo => new CouponProductInfo($name, $range, $validityText, $forLocale))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects\Coupon;
use Domain\Models\ValueObjects\Coupon\CouponProduct;
covers(CouponProduct::class);
describe('coupon product', function (): void {
it('creates a valid coupon product', function (): void {
// GIVEN
$name = 'Lorem';
$incoCodes = [];
$labels = [];
// WHEN
$couponProduct = new CouponProduct($name, $incoCodes, $labels);
// THEN
expect($couponProduct->name)->toBe($name)
->and($couponProduct->incoCodes)->toBe($incoCodes)
->and($couponProduct->labels)->toBe($labels);
});
it('rejects invalid coupon product', function (): void {
// GIVEN
$name = '';
$incoCodes = [];
$labels = [];
// GIVEN / THEN
expect(fn (): CouponProduct => new CouponProduct($name, $incoCodes, $labels))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\Coupon\CouponValidity;
use Shared\ValueObjects\DateTime;
covers(CouponValidity::class);
describe('coupon validity', function (): void {
it('creates a valid coupon validity', function (): void {
// GIVEN
$days = 30;
$until = new DateTime('2026-11-30');
// WHEN
$couponValidity = new CouponValidity($days, $until);
// THEN
expect($couponValidity->days)->toBe($days)
->and($couponValidity->until)->toBe($until);
});
it('rejects invalid coupon validity', function (): void {
// GIVEN
$days = 0;
$until = new DateTime('2026-11-30');
// GIVEN / THEN
expect(fn (): CouponValidity => new CouponValidity($days, $until))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\CouponName;
covers(CouponName::class);
describe('coupon name', function (): void {
it('creates a valid coupon name', function (): void {
// GIVEN
$value = 'Name';
// WHEN
$name = new CouponName($value);
// THEN
expect($name->value)->toBe($value);
});
it('rejects invalid coupon name', function (): void {
// GIVEN
$value = '';
// GIVEN / THEN
expect(fn (): CouponName => new CouponName($value))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\CouponProductName;
covers(CouponProductName::class);
describe('coupon product name', function (): void {
it('creates a valid coupon product name', function (): void {
// GIVEN
$value = 'Lorem';
// WHEN
$couponProductName = new CouponProductName($value);
// THEN
expect($couponProductName->value)->toBe($value);
});
it('rejects invalid coupon product name', function (): void {
// GIVEN
$value = '';
// GIVEN / THEN
expect(fn (): CouponProductName => new CouponProductName($value))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Enums\BarcodeFormat;
use Domain\Models\ValueObjects\GS1;
covers(GS1::class);
describe('gs1', function (): void {
it('creates a valid gs1', function (): void {
// GIVEN
$value = '1010100101';
$format = BarcodeFormat::EAN13;
// WHEN
$GS1 = new GS1($value, $format);
// THEN
expect($GS1->value)->toBe($value)
->and($GS1->format)->toBe($format);
});
it('rejects invalid gs1', function (): void {
// GIVEN
$value = '';
$format = BarcodeFormat::EAN13;
// GIVEN / THEN
expect(fn (): GS1 => new GS1($value, $format))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\IncoCode;
covers(IncoCode::class);
describe('inco code', function (): void {
it('creates a valid inco code', function (): void {
// GIVEN
$value = 1010100101;
// WHEN
$incoCode = new IncoCode($value);
// THEN
expect($incoCode->value)->toBe($value);
});
it('rejects invalid inco code', function (): void {
// GIVEN
$value = 0;
// GIVEN / THEN
expect(fn (): IncoCode => new IncoCode($value))->toThrow(\DomainException::class);
});
});

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\ValueObjects;
use Domain\Models\ValueObjects\ProcessingCode;
covers(ProcessingCode::class);
describe('processing code', function (): void {
it('creates a valid processing code', function (): void {
// GIVEN
$value = '1010100101';
// WHEN
$processingCode = new ProcessingCode($value);
// THEN
expect($processingCode->value)->toBe($value);
});
it('rejects invalid processing code', function (): void {
// GIVEN
$value = '';
// GIVEN / THEN
expect(fn (): ProcessingCode => new ProcessingCode($value))->toThrow(\DomainException::class);
});
});