public function test_peut_inscrire_participant_afup_day(): void
{
// Arrange
$participant = new Participant(
id: ParticipantId::generate(),
nom: new Nom('Lovelace'),
prenom: new Prenom('Ada'),
email: new Email('ada.lovelace@example.com'),
telephone: new NumeroTelephone('+33612345678'),
entreprise: new Entreprise('Acme Corp'),
adresseFacturation: new Adresse(
rue: new Rue('4 place du Théâtre'),
ville: new Ville('Lille'),
codePostal: new CodePostal('59000'),
pays: new Pays('France'),
),
);
$evenement = new Evenement(
id: EvenementId::generate(),
nom: new NomEvenement('AFUP Day 2026 Lille'),
date: new DateTimeImmutable('2026-05-22'),
lieu: new Lieu('La Comédie, Lille'),
capaciteMax: new Capacite(250),
organisateur: new Organisateur('AFUP Hauts-de-France'),
);
$confBcBreaks = new Conference(
titre: new Titre('Le progrès réside dans les BC Breaks'),
orateur: new Orateur('Gina Banyard'),
creneau: new Creneau(
debut: '2026-05-22 09:25',
dureeMinutes: 40,
),
salle: Salle::GRANDE_SALLE,
);
$confTests = new Conference(
titre: new Titre('Test simplifié à moitié pardonné'),
orateur: new Orateur('Romain Canon'),
creneau: new Creneau(
debut: '2026-05-22 14:50',
dureeMinutes: 40,
),
salle: Salle::GRANDE_SALLE,
);
$confFraude = new Conference(
titre: new Titre('2 secondes pour détecter la fraude'),
orateur: new Orateur('Mathieu Desnouveaux'),
creneau: new Creneau(
debut: '2026-05-22 12:05',
dureeMinutes: 40,
),
salle: Salle::GRANDE_SALLE,
);
$codePromo = new CodePromo(
code: new CodeCoupon('AFUP-EARLY-BIRD'),
reduction: new Reduction(15, TypeReduction::POURCENT),
valableJusquau: new DateTimeImmutable('2026-04-30'),
evenementsEligibles: [$evenement->id],
);
$billet = new Billet(
participant: $participant,
evenement: $evenement,
type: TypeBillet::JOURNEE,
prixHT: new Montant(85_00, Devise::EUR),
);
$panier = new PanierInscription($participant);
$panier->ajouterBillet($billet);
$panier->inscrireAConference($confBcBreaks);
$panier->inscrireAConference($confFraude);
$panier->inscrireAConference($confTests);
$panier->appliquerCodePromo($codePromo);
$panier->choisirMoyenPaiement(MoyenPaiement::CARTE_BANCAIRE);
$servicePaiement = $this->createMock(ServicePaiement::class);
$servicePaiement
->expects(self::once())
->method('debiter')
->with(self::isInstanceOf(Montant::class))
->willReturn(
new ResultatPaiement(
succes: true,
identifiantTransaction: 'txn_afup_42'
)
);
$envoyeurEmail = $this->createMock(EnvoyeurEmail::class);
$envoyeurEmail
->expects(self::exactly(3))
->method('envoyer');
$generateurBillet = $this->createMock(GenerateurBillet::class);
$generateurBillet
->expects(self::once())
->method('genererPdf')
->willReturn(new BilletPdf('TICKET-AFUP-2026-0042.pdf'));
$service = new ServiceInscription(
$servicePaiement,
$envoyeurEmail,
$generateurBillet,
new VerificateurCapacite(),
new ValidateurConflitsCreneaux(),
$this->createMock(LoggerInterface::class),
);
// Act
$inscription = $service->inscrire($panier);
// Assert
self::assertInstanceOf(Inscription::class, $inscription);
self::assertSame(StatutInscription::CONFIRMEE, $inscription->statut);
self::assertSame($participant, $inscription->participant);
self::assertSame($evenement, $inscription->evenement);
self::assertNotNull($inscription->confirmeeLe);
self::assertCount(3, $inscription->conferences);
self::assertEquals(
new Montant(85_00, Devise::EUR),
$inscription->prixHT
);
self::assertEquals(
new Montant(12_75, Devise::EUR),
$inscription->montantReduction
);
self::assertEquals(
new Montant(14_45, Devise::EUR),
$inscription->montantTVA
);
self::assertEquals(
new Montant(86_70, Devise::EUR),
$inscription->totalTTC
);
self::assertSame(
'txn_afup_42',
$inscription->identifiantTransaction
);
self::assertSame(
'TICKET-AFUP-2026-0042.pdf',
$inscription->cheminBillet
);
}
|
|
@Romm |
|
|
@romain-canon |
|
|
@Romm@mastodon.social |
|
|
@romain-canon.com |
PHPUnit • Codeception • Behat • PHPSpec
Couverture de tests élevée ≠ efficacité
Privilégier le Mutation Testing et le MSI
Aident à détecter des bugs dans les tests
Améliorent la confiance accordée aux tests
PER Coding Style (PSR-1 / PSR-12)
Forcer le style → fin du débat
❌ test_it_works
✅ test_product_can_be_added_to_cart
| Arrange | — | Initialisation des données |
| Act | — | Exécution du composant |
| Assert | — | Validation du résultat |
public function test_can_bind_address_to_customer()
{
// Arrange
$customer = new Customer(
name: new Name('Ada Lovelace'),
birthdate: new DateTimeImmutable('1971-11-08'),
email: new Email('ada.lovelace@example.com'),
phone: new PhoneNumber('+33612345678'),
);
$address = new Address(
street: new Street('221B Baker Street'),
city: new City('London'),
zipCode: new ZipCode('NW1 6XE'),
country: new Country('United Kingdom'),
);
// Act
$customer = $customer->bindAddress($address);
// Assert
self::assertSame($address, $customer->address);
}
final class Customer
{
public function __construct(
private(set) Name $name,
private(set) DateTimeInterface $birthDate,
private(set) Email $email,
private(set) PhoneNumber $phoneNumber,
// Nouvelle propriété
private(set) Nationality $nationality,
) {}
}
Entities
Data Transfer Objects
Value Objects
public function test_can_bind_address_to_customer()
{
// Arrange
$customer = FakeCustomer::new(); // 😌
$address = FakeAddress::new(); // 😌
// Act
$customer = $customer->bindAddress($address);
// Assert
self::assertSame($address, $customer->address);
}
“An Object Mother is a kind of class used in testing to help create example objects that you use for testing”
« Un Object Mother est un type de classe utilisée dans les tests pour aider à créer des objets d'exemple utilisés pour tester »
final class Email
{
public function __construct(private string $value)
{
// Some validation here…
}
}
final class FakeEmail
{
public static function new(
?string $email = null
): Email {
return new Email($email ?? 'jane.doe@example.com');
}
}
public function test_can_do_something_with_email()
{
// Arrange
$email = FakeEmail::new();
// Act
// Assert
}
public function test_can_do_something_with_email()
{
// Arrange
$email = FakeEmail::new('ada.lovelace@example.com');
// Act
// Assert
}
final class FakePhoneNumber
{
public static function new(
?string $phone = null
): PhoneNumber {
return new PhoneNumber($phone ?? '+33612345678');
}
}
final class FakeDate
{
public static function new(
?string $date = null
): DateTimeInterface {
return new DateTimeImmutable($date ?? '1971-11-08');
}
}
final class FakeCustomer
{
public static function new(
?string $name = null,
?string $birthDate = null,
?string $email = null,
?string $phone = null,
): Customer {
return new Customer(
name: FakeName::new($name),
birthDate: FakeDate::new($birthDate),
email: FakeEmail::new($email),
phone: FakePhoneNumber::new($phone),
);
}
}
public function test_can_bind_address_to_customer()
{
// Arrange
$customer = FakeCustomer::new(); // 😌
$address = FakeAddress::new(); // 😌
// Act
$customer = $customer->bindAddress($address);
// Assert
self::assertSame($address, $customer->address);
}
public function test_can_spot_afup_member()
{
// Arrange
$amelie = FakeCustomer::new(email: 'amelie@afup.org');
$nathan = FakeCustomer::new(email: 'nathan@botron.fr');
// Assert
self::assertTrue($amelie->isAfupMember);
self::assertFalse($nathan->isAfupMember);
}
final class FakeCustomer
{
public static function new(…) { … }
public static function nathan_botron(): Customer
{
return self::new(
name: 'Nathan Botron',
birthDate: '1990-08-06',
email: 'nathan.botron@example.com',
phone: '0612345678',
);
}
}
public function test_can_bind_address_to_customer()
{
// Arrange
$customer = FakeCustomer::nathan_botron();
$address = FakeAddress::abbey_road();
// Act
// Assert
}
final class FakeCustomer
{
public static function new(
?string $name = null,
?string $birthDate = null,
?string $email = null,
?string $phone = null,
?string $nationality = null,
): Customer {
return new Customer(
name: FakeName::new($name),
birthDate: FakeDate::new($birthDate),
email: FakeEmail::new($email),
phone: FakePhoneNumber::new($phone),
nationality: FakeNationality::new($nationality),
);
}
}
$ composer require --dev fakerphp/faker
final class FakeEmail
{
public static function new(…) {…}
public static function random(): Email
{
return new Email(
email: faker()->email()
// nicolas.enid@block.com
// dooley.freddie@gmail.com
// leonardo55@rempel.net
// …
);
}
}
final class FakePhoneNumber
{
public static function new(…) {…}
public static function random(): PhoneNumber
{
return new PhoneNumber(
phone: faker()->phoneNumber()
// +33 3 22 15 53 93
// +33 6 82 26 45 81
// +33 2 64 27 30 34
// …
);
}
}
final class FakeCustomer
{
public static function new(…) {…}
public static function nathan_botron() {…}
public static function random(): Customer
{
return new Customer(
name: FakeName::random(),
birthDate: FakeDate::random(),
email: FakeEmail::random(),
phone: FakePhoneNumber::random(),
nationality: FakeNationality::random(),
);
}
}
public function test_can_do_something_with_many_customers()
{
// Arrange
$customerA = FakeCustomer::random();
$customerB = FakeCustomer::random();
$customerC = FakeCustomer::random();
$customerD = FakeCustomer::random();
$customerE = FakeCustomer::random();
// …
// Act
// Assert
}
final class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
// On veut toujours les retrouver
// dans notre application :
$manager->persist(
FakeCustomer::nathan_botron(),
FakeCustomer::jane_doe(),
FakeCustomer::ada_lovelace(),
);
// Et 10 autres aléatoires…
for ($i = 0; $i < 10; $i++) {
$manager->persist(FakeCustomer::random());
}
$manager->flush();
}
}
“A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.”
$ composer require --dev zenstruck/foundry
Valorisez vos tests comme votre code applicatif
Ne délaissez pas leur maintenabilité
Prenez soin de vos tests, ils vous le rendront 💅
Donne tes impressions 🙏
|
|
@Romm |
|
|
@romain-canon |
|
|
@Romm@mastodon.social |
|
|
@romain-canon.com |
Par ici pour revoir les slides
use PHPUnit\Event\Test\PreparationStarted;
use PHPUnit\Event\Test\PreparationStartedSubscriber;
class FixedSeed implements PreparationStartedSubscriber
{
/**
* This subscriber is specifically tied to the use of
* Faker and is triggered before each PHPUnit test is
* executed. It reassigns the seed for Faker's random
* functions based on the test's signature, ensuring
* that the same seed is used consistently for each
* test run. As a result, Faker generates the same
* random values for a given test, provided the test
* signature remains unchanged.
*/
public function notify(PreparationStarted $event)
{
$hash = crc32($event->test()->id());
faker()->seed($hash);
}
}