Builder Pattern : Créer des objets de test complexes avec clarté
Cet article est le quatrième de notre série dédiée aux design patterns pour l'automatisation des tests. Après avoir exploré le Page Object Model, le Factory Pattern et le Facade Pattern, découvrons comment le Builder Pattern peut révolutionner la création de nos objets de test complexes.

Quand la construction devient un art
Imaginez-vous en train de créer un objet de test avec une dizaine de propriétés, où certaines sont obligatoires, d'autres optionnelles, et où différentes combinaisons créent différents scénarios de test. Les constructeurs traditionnels deviennent rapidement un cauchemar : trop de paramètres, ordre à retenir, valeurs par défaut perdues dans la masse...
Le Builder Pattern transforme cette construction chaotique en un processus fluide et intuitif. Tel un architecte qui dessine les plans étape par étape avant de construire, ce pattern nous permet de créer nos objets complexes avec clarté et flexibilité.
Qu'est-ce que le Builder Pattern ?
Le Builder Pattern est un pattern de création qui permet de construire des objets complexes étape par étape en utilisant une interface fluide. Il sépare la construction d'un objet de sa représentation, permettant au même processus de création de produire différentes représentations.
Objectif principal : Faciliter la personnalisation d'objets complexes
Le Builder Pattern répond à des besoins cruciaux en automatisation de tests :
- Lisibilité : Interface fluide et expressive pour la création d'objets
- Flexibilité : Construction personnalisable avec options modulaires
- Validation : Contrôles de cohérence lors de la construction
- Réutilisabilité : Templates réutilisables pour différents scénarios
Architecture du Builder Pattern
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT (Tests) │
│ │
│ TestA ──── new UserBuilder() │
│ .withName("John") ┌─────────────┐ │
│ .withAge(30) ────▶ │ BUILDER │ │
│ .withPremium() │ │ │
│ .build() │ │ │
└─────────────────────────────────────────┼─────────────┼────────┘
│ │
│ │
┌─────────────────────────────────────────┼─────────────┼────────┐
│ BUILDER │ │ │
│ │ │ │
│ ┌─────────────────────────────────────┐│ │ │
│ │ ConcreteBuilder ││ │ │
│ │ ││ │ │
│ │ - name: String ││ │ │
│ │ - age: int ││ │ │
│ │ - premium: boolean ││ │ │
│ │ ││ │ │
│ │ + withName(String): Builder ││ │ │
│ │ + withAge(int): Builder ││ │ │
│ │ + withPremium(): Builder ││ │ │
│ │ + build(): Product ││ │ │
│ └─────────────────────────────────────┘│ │ │
└─────────────────────────────────────────┼─────────────┼────────┘
│ │
│ construit │
▼ │
┌─────────────────────────────────────────────────────────────────┐
│ PRODUCT │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ User Object │ │
│ │ │ │
│ │ - name: String │ │
│ │ - age: int │ │
│ │ - premium: boolean │ │
│ │ - subscriptions: List<Subscription> │ │
│ │ - preferences: UserPreferences │ │
│ │ │ │
│ │ + getName(): String │ │
│ │ + getAge(): int │ │
│ │ + isPremium(): boolean │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Exemple concret : Souscription à une assurance avec options
Prenons l'exemple d'une application d'assurance où nous devons tester différents scénarios de souscription. Les polices d'assurance peuvent avoir de nombreuses options et configurations.
Sans Builder Pattern (approche complexe et confuse) :
// Constructeur avec trop de paramètres - impossible à maintenir !
@Test
public void testBasicInsuranceSubscription() {
// Quel paramètre fait quoi ? Impossible à lire...
InsurancePolicy policy = new InsurancePolicy(
"John Doe", // name
30, // age
"123 Main St", // address
"Sedan", // vehicleType
2020, // vehicleYear
true, // hasGarage
false, // hasPreviousClaims
500.0, // deductible
true, // comprehensiveCoverage
false, // rentalCarCoverage
true, // roadAssistance
null, // additionalDrivers
"monthly" // paymentFrequency
);
// Test difficile à comprendre...
assertTrue(policy.isValid());
}
@Test
public void testPremiumInsuranceSubscription() {
// Encore plus de confusion avec des valeurs null partout
InsurancePolicy policy = new InsurancePolicy(
"Jane Smith", 25, "456 Oak Ave", "SUV", 2022, false, true,
250.0, true, true, true, Arrays.asList("Spouse"), "annual"
);
assertTrue(policy.isPremium());
}
Avec Builder Pattern (approche claire et lisible) :
// Builder simple pour créer des polices d'assurance
public class InsurancePolicyBuilder {
private String customerName;
private int customerAge;
private String vehicleType;
private boolean hasRoadAssistance = false;
private boolean hasRentalCoverage = false;
private double deductible = 500.0;
// Méthodes pour construire étape par étape
public InsurancePolicyBuilder forCustomer(String name, int age) {
this.customerName = name;
this.customerAge = age;
return this; // Retourne this pour chaîner les appels
}
public InsurancePolicyBuilder forVehicle(String type) {
this.vehicleType = type;
return this;
}
public InsurancePolicyBuilder withRoadAssistance() {
this.hasRoadAssistance = true;
return this;
}
public InsurancePolicyBuilder withRentalCoverage() {
this.hasRentalCoverage = true;
return this;
}
public InsurancePolicyBuilder withDeductible(double amount) {
this.deductible = amount;
return this;
}
// Packages prédéfinis pour simplifier
public InsurancePolicyBuilder asPremiumPackage() {
this.hasRoadAssistance = true;
this.hasRentalCoverage = true;
this.deductible = 250.0;
return this;
}
public InsurancePolicyBuilder asBasicPackage() {
this.hasRoadAssistance = true;
this.hasRentalCoverage = false;
this.deductible = 500.0;
return this;
}
// Création finale de l'objet
public InsurancePolicy build() {
return new InsurancePolicy(customerName, customerAge, vehicleType,
hasRoadAssistance, hasRentalCoverage, deductible);
}
}
// Classe InsurancePolicy simplifiée
public class InsurancePolicy {
private final String customerName;
private final int customerAge;
private final String vehicleType;
private final boolean hasRoadAssistance;
private final boolean hasRentalCoverage;
private final double deductible;
public InsurancePolicy(String customerName, int customerAge, String vehicleType,
boolean hasRoadAssistance, boolean hasRentalCoverage, double deductible) {
this.customerName = customerName;
this.customerAge = customerAge;
this.vehicleType = vehicleType;
this.hasRoadAssistance = hasRoadAssistance;
this.hasRentalCoverage = hasRentalCoverage;
this.deductible = deductible;
}
public boolean isPremium() {
return hasRoadAssistance && hasRentalCoverage && deductible <= 250;
}
public double calculateMonthlyPremium() {
double premium = 100.0;
if (customerAge < 25) premium += 50.0;
if (hasRoadAssistance) premium += 15.0;
if (hasRentalCoverage) premium += 25.0;
return premium;
}
// Getters
public String getCustomerName() { return customerName; }
public boolean isPremium() { return hasRoadAssistance && hasRentalCoverage; }
}
// Tests utilisant le Builder - Simple et clair !
@Test
public void testBasicInsurance() {
InsurancePolicy policy = new InsurancePolicyBuilder()
.forCustomer("John Doe", 30)
.forVehicle("Sedan")
.asBasicPackage()
.build();
assertFalse(policy.isPremium());
assertEquals("John Doe", policy.getCustomerName());
}
@Test
public void testPremiumInsurance() {
InsurancePolicy policy = new InsurancePolicyBuilder()
.forCustomer("Jane Smith", 25)
.forVehicle("SUV")
.asPremiumPackage()
.build();
assertTrue(policy.isPremium());
assertTrue(policy.calculateMonthlyPremium() > 150);
}
@Test
public void testCustomInsurance() {
InsurancePolicy policy = new InsurancePolicyBuilder()
.forCustomer("Alex Young", 22)
.forVehicle("Compact")
.withRoadAssistance()
.withDeductible(1000.0)
.build();
assertEquals("Alex Young", policy.getCustomerName());
}
Comparaison des approches :
Sans Builder : Constructeur illisible avec 12+ paramètres dans un ordre impossible à retenir Avec Builder : Interface fluide et expressive qui lit comme du langage naturel
// Sans Builder (confus)
new InsurancePolicy("John", 30, "123 Main St", "Sedan", 2020, true, false,
500.0, true, false, true, null, "monthly");
// Avec Builder (clair)
new InsurancePolicyBuilder()
.forCustomer("John Doe", 30)
.forVehicle("Sedan", 2020)
.withGarage()
.asPremiumPackage()
.build();
Ici, chaque méthode est explicite. Le test est beaucoup plus lisible et compréhensible.
Builder Pattern avancé : Templates et Factory Integration
// Factory de builders pour des scénarios prédéfinis
public class InsurancePolicyTemplates {
public static InsurancePolicyBuilder youngDriverTemplate() {
return new InsurancePolicyBuilder()
.withDeductible(1000.0) // Franchise élevée pour réduire le coût
.asBasicPackage();
}
public static InsurancePolicyBuilder seniorDriverTemplate() {
return new InsurancePolicyBuilder()
.withDeductible(250.0) // Franchise faible
.asPremiumPackage()
.withAnnualPayment(); // Généralement paiement annuel
}
public static InsurancePolicyBuilder luxuryVehicleTemplate() {
return new InsurancePolicyBuilder()
.withDeductible(500.0)
.asPremiumPackage()
.withAdditionalDriver("Valet"); // Service de voiturier
}
public static InsurancePolicyBuilder fleetTemplate() {
return new InsurancePolicyBuilder()
.withDeductible(1000.0)
.withComprehensiveCoverage()
.withAnnualPayment();
}
}
// Utilisation des templates dans les tests
@Test
public void testYoungDriverScenarios() {
InsurancePolicy policy = InsurancePolicyTemplates.youngDriverTemplate()
.forCustomer("Alex Young", 19)
.forVehicle("Compact", 2015)
.build();
assertTrue(policy.calculateMonthlyPremium() > 100);
assertFalse(policy.isPremium());
}
@Test
public void testLuxuryVehicleScenario() {
InsurancePolicy policy = InsurancePolicyTemplates.luxuryVehicleTemplate()
.forCustomer("Robert Rich", 45)
.forVehicle("BMW", 2023)
.withGarage()
.build();
assertTrue(policy.isPremium());
assertTrue(policy.calculateMonthlyPremium() > 500);
}
Architecture complète avec Builder Pattern

Bonnes pratiques essentielles
1. Interface fluide et expressive
Nommez vos méthodes pour qu'elles se lisent comme du langage naturel :
// ✅ Nommage expressif et fluide
public InsurancePolicyBuilder forCustomer(String name, int age) { }
public InsurancePolicyBuilder forVehicle(String type, int year) { }
public InsurancePolicyBuilder withPreviousClaims() { }
public InsurancePolicyBuilder asPremiumPackage() { }
// ❌ Nommage technique peu clair
public InsurancePolicyBuilder setName(String name) { }
public InsurancePolicyBuilder vehicleData(String type, int year) { }
public InsurancePolicyBuilder claimsTrue() { }
2. Validation intelligente
Implémentez une validation robuste au moment du build :
public InsurancePolicy build() {
validateRequired();
validateBusinessRules();
validateCombinations();
return new InsurancePolicy(this);
}
private void validateRequired() {
if (customerName == null || customerName.trim().isEmpty()) {
throw new IllegalStateException("Customer name is required");
}
}
private void validateBusinessRules() {
if (customerAge < 18) {
throw new IllegalStateException("Customer must be at least 18 years old");
}
if (deductible < 0) {
throw new IllegalStateException("Deductible cannot be negative");
}
}
private void validateCombinations() {
if (vehicleYear < 2000 && comprehensiveCoverage) {
throw new IllegalStateException("Comprehensive coverage not available for vehicles older than 2000");
}
}
3. Méthodes de commodité et templates
Fournissez des raccourcis pour les configurations communes :
public InsurancePolicyBuilder asPremiumPackage() {
return this.withComprehensiveCoverage()
.withRentalCarCoverage()
.withRoadAssistance()
.withDeductible(250.0);
}
public InsurancePolicyBuilder forSeniorDriver() {
return this.withDeductible(250.0)
.asPremiumPackage()
.withAnnualPayment();
}
public InsurancePolicyBuilder forStudent() {
return this.withDeductible(1000.0)
.asBasicPackage()
.withMonthlyPayment();
}
4. Builder réutilisable avec reset
Permettez la réutilisation du même builder :
public InsurancePolicyBuilder reset() {
this.customerName = null;
this.customerAge = 0;
this.hasGarage = false;
this.deductible = 500.0;
this.additionalDrivers.clear();
// Reset autres propriétés...
return this;
}
// Utilisation
InsurancePolicyBuilder builder = new InsurancePolicyBuilder();
InsurancePolicy policy1 = builder
.forCustomer("John", 30)
.forVehicle("Sedan", 2020)
.build();
InsurancePolicy policy2 = builder
.reset()
.forCustomer("Jane", 25)
.forVehicle("SUV", 2022)
.build();
5. Intégration avec les tests paramétrés
Combine Builder avec les tests paramétrés :
@ParameterizedTest
@MethodSource("insuranceScenarios")
public void testInsurancePremiumCalculation(String scenario,
Function<InsurancePolicyBuilder, InsurancePolicyBuilder> builderConfig,
double expectedMinPremium) {
InsurancePolicy policy = builderConfig
.apply(new InsurancePolicyBuilder())
.build();
assertTrue(policy.calculateMonthlyPremium() >= expectedMinPremium,
"Premium for " + scenario + " should be at least " + expectedMinPremium);
}
static Stream<Arguments> insuranceScenarios() {
return Stream.of(
Arguments.of("Young Driver",
(Function<InsurancePolicyBuilder, InsurancePolicyBuilder>) builder ->
builder.forCustomer("Young Driver", 19)
.forVehicle("Compact", 2015)
.asBasicPackage(),
150.0),
Arguments.of("Senior Premium",
(Function<InsurancePolicyBuilder, InsurancePolicyBuilder>) builder ->
builder.forCustomer("Senior Driver", 65)
.forVehicle("Luxury", 2023)
.asPremiumPackage(),
300.0)
);
}
FAQ
Q: Quand utiliser le Builder Pattern dans mes tests ?
R: Utilisez-le pour des objets avec 4+ propriétés optionnelles, des configurations multiples, ou quand vous avez besoin de plusieurs variations du même objet. C'est parfait pour les données de test complexes (utilisateurs, commandes, configurations).
Q: Builder Pattern vs Factory Pattern, quelle différence ?
R: Factory crée des objets complets d'un type donné, Builder construit des objets personnalisables étape par étape. Factory = "créer différents types", Builder = "personnaliser un type complexe".
Q: Comment gérer les propriétés obligatoires avec Builder ?
R: Plusieurs approches : validation dans build(), constructeur du Builder avec paramètres obligatoires, ou méthodes chaînées obligatoires (forCustomer() avant build()).
Q: Le Builder Pattern impacte-t-il les performances ?
R: Impact minimal. Le coût de création d'un objet Builder est négligeable comparé aux bénéfices en lisibilité et maintenabilité. Pour optimiser, réutilisez les builders avec reset().
Q: Comment tester mon Builder lui-même ?
R: Testez la validation (cas d'erreur), les valeurs par défaut, et les méthodes de commodité. Vérifiez que build() produit l'objet attendu selon la configuration.
Q: Peut-on combiner Builder avec d'autres patterns ?
R: Absolument ! Builder + Factory (templates), Builder + Strategy (différents algorithmes de validation), Builder + Prototype (clonage de configurations).
Q: Comment documenter efficacement mes Builders ?
R: Documentez les propriétés requises vs optionnelles, les valeurs par défaut, les méthodes de commodité disponibles, et donnez des exemples d'utilisation typiques pour chaque scénario métier.
Q: Le Builder n’alourdit-il pas mon framework ?
R: Au contraire : il clarifie le code. Le surcoût est minime, et les bénéfices en lisibilité et en maintenabilité sont énormes.
Q: Puis-je combiner Builder et Facade ?
R: Oui. Par exemple, une Facade e-commerce peut utiliser un Builder pour préparer des profils utilisateurs ou des contrats avant l’exécution d’un scénario.
A retenir
Le Builder Pattern est un allié incontournable dès que vous devez gérer des objets de test riches et personnalisables.
Il transforme vos tests en scripts lisibles, robustes et simples à maintenir.
Dans le prochain article, nous découvrirons le Screenplay Pattern, une approche révolutionnaire pour structurer nos tests avec une lisibilité et une robustesse exceptionnelles.