Test simplifié
à moitié pardonné

Romain Canon Profile picture

2020 — Jetbrains Developer Survey

 → 64% des répondants écrivent des tests en PHP


2025 — Jetbrains Developer Survey

 → 68% des répondants écrivent des tests en PHP

Pourquoi
testons-nous
nos applications ?

Pourquoi
testons-nous
nos applications ?

  1. Garantir leur bon fonctionnement
  2. Décrire les résultats attendus des composants
  3. Éviter au maximum les régressions

Avec quels outils ?

Avec quels outils ?

PHPUnit • Codeception • Behat • PHPSpec

Quelques pièges
à éviter

Couverture de tests

Couverture de tests

Couverture de tests élevée ≠ efficacité

Privilégier le Mutation Testing et le MSI

Cibler les composants testés

Cibler les composants testés

🛒
Validation du panier
💸 Revenu direct
VS
🤳
Changement de photo de profil
💤 Cosmétique

Attention aux mocks

Attention aux mocks

Mock Mock

Et la maintenabilité
dans tout ça ?

Les tests devraient répondre
aux mêmes critères de qualité
que le code applicatif

Partager les outils d'analyse statique

Partager les outils d'analyse statique


PHPStan • Psalm • Mago

 Aident à détecter des bugs dans les tests

 Améliorent la confiance accordée aux tests

Partager les outils d'analyse statique


PHP-CS-Fixer • PHP_CodeSniffer

PER Coding Style (PSR-1 / PSR-12)

 Forcer le style → fin du débat

Clarifier le nom
des tests

Clarifier le nom
des tests

❌ test_it_works

✅ test_product_can_be_added_to_cart

Attention à
la lourdeur des tests

Il est difficile
pour un test trop long…

Il est difficile
pour un test trop long…

  • …de comprendre son utilité
  • …d'identifier le problème lorsqu'il plante
  • …de le maintenir lorsque le code évolue :
    • → Nouvelle fonctionnalité
    • → Refactor

Un test trop long peut…

Un test trop long peut…

  • …être découpé en plusieurs tests
  • …faire usage des « data providers »
  • …suivre le pattern « A∙A∙A »

Pattern « A∙A∙A »

Pattern « A∙A∙A »

Arrange Initialisation des données
Act Exécution du composant
Assert Validation du résultat

A∙A∙A : Arrange

Là où ça bouchonne

								
									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);
									}
								
							
  • 😨 X 10
  • 😱 x 100
  • 🥵 x 1000
							
								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,
									) {}
								}
							
						
  • 😨 X 10
  • 😱 x 100
  • 🥵 x 1000

Améliorer l'initialisation des données

Améliorer l'initialisation des données

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);
								}
							
						

Object Mother Pattern

Object Mother Pattern


“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 »


https://martinfowler.com/bliki/ObjectMother.html

							
								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
								}
							
						

Une nouvelle propriété ?

Une nouvelle propriété ?

							
								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),
										);
									}
								}
							
						

Dynamiser les données

Dynamiser les données


FakerPHP.org


$ 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
							}
						
					

Utilisation
dans les Fixtures

						
							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();
								}
							}
						
					

Foundry

Foundry

“A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.”

$ composer require --dev zenstruck/foundry

En résumé

En résumé

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 🙏

QR code feedback

Merci !

Par ici pour revoir les slides

QR code du talk

Préserver
les mêmes données

							
								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);
									}
								}