> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Tests par propriété : générateurs custom pour des données réalistes
- URL: https://www.sfeir.dev/back/tests-par-propriete-generateurs-custom-pour-des-donnees-realistes/
- Published: 2026-06-24T12:35:08.000Z
- Updated: 2026-06-24T12:51:53.000Z
- Description: Après avoir découvert les bases des tests par propriété avec jqwik, il est temps de passer à l'étape suivante : la génération de données réalistes. Emails, dates, objets métier ou utilisateurs complets, découvrez comment créer des générateurs personnalisés.
- Author: Erwan Le Tutour
- Tags: Back, Java, Test, Test unitaire, Testing

[Dans l'article précédent](https://www.sfeir.dev/back/decouvrir-les-tests-par-propriete-avec-java-et-jqwik/), nous avons découvert les tests par propriété avec jqwik. Nous avons vu comment générer automatiquement des nombres, des chaînes de caractères ou encore des collections afin de vérifier qu'une règle métier reste toujours vraie.

Cette approche est déjà très puissante, mais dans une application réelle nous manipulons rarement des entiers ou des chaînes isolées. Nous travaillons plutôt avec des adresses email, des dates, des utilisateurs ou encore des objets complexes.

Heureusement, jqwik permet de créer ses propres générateurs afin de produire des données beaucoup plus proches de la réalité métier.

## Pourquoi créer ses propres générateurs ?

Prenons l'exemple d'un service qui valide une adresse email.

Un premier réflexe pourrait être d'utiliser simplement :

```
@Propertyvoid emailShouldBeValid(@ForAll String email) {    ...}

```

Le problème est que jqwik va générer toutes sortes de chaînes :

```
"abc"
""
"123"
"@"
"#$%^"

```

Ces valeurs peuvent être intéressantes pour tester la robustesse d'un système, mais elles ne correspondent pas forcément au contexte métier que nous souhaitons valider.  
Dans certains cas, il est plus pertinent de produire directement des données respectant les contraintes attendues.

C'est exactement le rôle des générateurs personnalisés.

## Générer des données cohérentes avec le métier

L'objectif n'est pas seulement de produire des valeurs aléatoires.

Un bon générateur doit produire :

- des données réalistes ;
- des données variées ;
- des données conformes aux contraintes métier ;
- suffisamment de cas limites pour détecter les anomalies.

Autrement dit, nous cherchons à reproduire le comportement d'utilisateurs réels tout en conservant la puissance de l'exploration automatique.

### Exemple 1 : générer des adresses email valides

Pour créer un générateur custom dans jqwik, nous allons avoir besoin de trois éléments : [une annotation](https://www.sfeir.dev/back/comprendre-les-annotations-dans-spring-boot/), un provider, et un mécanisme d'enregistrement.

Commençons par l'annotation qui viendra décorer nos paramètres de test :

```java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Email {
    String[] domains() default {};
    int maxLocalLength() default 20;
}

```

Les deux paramètres optionnels permettent de contraindre la génération selon le contexte : si `domains` est renseigné, le générateur piochera dans cette liste ; sinon, il construira des domaines aléatoires.

Passons maintenant au provider, qui implémente `ArbitraryProvider` :

```java
public class EmailGenerator implements ArbitraryProvider {

    @Override
    public boolean canProvideFor(TypeUsage targetType) {
        return targetType.isOfType(String.class)
            && targetType.findAnnotation(Email.class).isPresent();
    }

    @Override
    public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
        Email annotation = targetType.findAnnotation(Email.class).orElseThrow();
        return Set.of(buildEmailArbitrary(annotation));
    }

    private Arbitrary<String> buildEmailArbitrary(Email annotation) {
        Arbitrary<String> localPart = Arbitraries.strings()
            .withChars("abcdefghijklmnopqrstuvwxyz0123456789._")
            .ofMinLength(1)
            .ofMaxLength(annotation.maxLocalLength())
            .filter(s -> !s.startsWith(".") && !s.startsWith("_")
                      && !s.endsWith(".")   && !s.endsWith("_"));

        Arbitrary<String> domain = buildDomainArbitrary(annotation);

        return Combinators.combine(localPart, domain)
            .as((local, dom) -> local + "@" + dom);
    }

    private Arbitrary<String> buildDomainArbitrary(Email annotation) {
        if (annotation.domains().length > 0) {
            return Arbitraries.of(annotation.domains());
        }

        Arbitrary<String> domainName = Arbitraries.strings()
            .withChars("abcdefghijklmnopqrstuvwxyz0123456789-")
            .ofMinLength(2)
            .ofMaxLength(15)
            .filter(s -> !s.startsWith("-") && !s.endsWith("-"));

        Arbitrary<String> tld = Arbitraries.of("com", "org", "net", "io", "fr", "de", "uk");

        return Combinators.combine(domainName, tld)
            .as((name, ext) -> name + "." + ext);
    }

    @Override
    public int priority() {
        return 10;
    }
}

```

La méthode `canProvideFor` est la **clé** du mécanisme : elle s'assure que ce provider ne s'active que lorsque le paramètre est à la fois un `String` et annoté avec `@Email`, évitant ainsi tout effet de bord sur les autres générateurs.

Il reste ensuite à déclarer ce provider à jqwik. C'est ici qu'intervient le mécanisme **SPI** (*Service Provider Interface*), intégré à Java depuis le JDK 1.3\. Plutôt que de câbler en dur les implémentations d'une interface dans le code, Java permet de les déclarer dans un fichier texte que le `ServiceLoader` découvre et instancie automatiquement au démarrage.

Il faut donc créer le fichier suivant dans `src/test/resources` :

```
META-INF/services/net.jqwik.api.providers.ArbitraryProvider

```

Avec pour contenu le nom complet de notre provider :

```
com.example.EmailGenerator

```

Notre générateur est maintenant opérationnel. Nous pouvons l'utiliser dans nos tests :

```java
private static final Pattern EMAIL_PATTERN = Pattern.compile(
        "^[a-zA-Z0-9][a-zA-Z0-9._-]*@[a-zA-Z0-9-]+\\.[a-zA-Z]{2,}$"
);

@Property
void emailShouldBeValid(@ForAll @Email String email) {
    assertTrue(EMAIL_PATTERN.matcher(email).matches());
}

@Property
void emailShouldUseCorporateDomain(@ForAll @Email(domains = {"company.com", "corp.io"}) String email) {
    assertTrue(email.endsWith(".com") || email.endsWith(".io"));
}
```

À noter que la propriété testée reste **indépendante de l'implémentation du générateur** : la regex de validation et la logique de construction dans `EmailGenerator` sont deux choses distinctes.  
Si le générateur produisait un email incorrect, le test le détecterait immédiatement.

### Exemple 2 : générer des dates dans un intervalle

Les dates sont un autre cas très fréquent en contexte métier. Une date de naissance ne peut pas être dans le futur, une date de contrat doit se situer dans une plage cohérente, une échéance ne peut pas précéder sa date de création. Autant de contraintes que les générateurs par défaut de jqwik ne connaissent pas.

Plutôt que d'implémenter un `ArbitraryProvider` complet comme pour les emails, jqwik offre une approche plus légère pour ce type de cas : les **méthodes de génération nommées**. Il suffit de créer une méthode annotée `@Provide` dans la classe de test, puis de la référencer par son nom dans `@ForAll`.

Commençons par extraire la logique de génération dans une classe utilitaire réutilisable :

```java
public class DateGenerators {  
    public static Arbitrary<LocalDate> datesBetween(LocalDate start, LocalDate end) {  
        return Arbitraries.longs()  
                .between(start.toEpochDay(), end.toEpochDay())  
                .map(LocalDate::ofEpochDay);  
    }  
}

```

L'astuce repose sur `toEpochDay()` : plutôt que de manipuler des dates directement, on génère un `long` représentant le nombre de jours écoulés depuis le 1er janvier 1970, puis on le reconvertit en `LocalDate` via `ofEpochDay`.  
Cela permet de bénéficier de toute la puissance des générateurs numériques de jqwik tout en obtenant des dates en sortie.

Dans la classe de test, on expose ensuite ce générateur via `@Provide` :

```java

@Property
void birthdayShouldBeInThePast(@ForAll("pastDates") LocalDate birthday) {
	assertTrue(birthday.isBefore(LocalDate.now()));
}

@Provide
Arbitrary<LocalDate> pastDates() {
	return DateGenerators.datesBetween(
		LocalDate.of(1900, 1, 1),
		LocalDate.of(2026, 1, 1)
	);
}

```

La valeur passée à `@ForAll("pastDates")` est simplement le nom de la méthode annotée `@Provide`, jqwik se charge de faire le lien automatiquement.  
Cette approche est particulièrement adaptée quand la contrainte est **ponctuelle et contextuelle** : on n'a pas besoin d'un provider global enregistré via SPI, juste d'une méthode utilitaire réutilisable selon les besoins du test.

---

Pour les cas simples de génération de dates, jqwik propose le module `jqwik-time` qui évite d'écrire tout ce code. Après l'ajout de la dépendance, l'exemple précédent se résume à :

```java
@Property
void birthdayShouldBeInThePast(@ForAll @DateRange(min = "1900-01-01", max = "2026-01-01") LocalDate birthday) {
    assertTrue(birthday.isBefore(LocalDate.now()));
}

```

Le module couvre une large gamme de types : `LocalDate`, `LocalTime`, `LocalDateTime`, `Instant`, `ZonedDateTime`, `YearMonth`... avec des annotations dédiées pour chacun.  
L'exemple `datesBetween` reste néanmoins pertinent dès que les contraintes deviennent plus complexes qu'un simple intervalle : par exemple, combiner une plage de dates avec d'autres règles métier.

---

### Exemple 3 : générer des objets métier complets

Jusqu'ici, nous avons généré des types simples : des `String` et des `LocalDate`. Dans une application réelle, les propriétés que l'on souhaite vérifier portent le plus souvent sur des objets métier complets. jqwik permet de les générer tout aussi facilement grâce à `Combinators.combine()`.

Prenons l'exemple d'un objet `User` :

```java
public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
}

```

Le générateur associé combine plusieurs arbitraires indépendants pour construire une instance cohérente :

```java
public class UserGenerator {

    public static Arbitrary<User> users() {
        return Combinators.combine(
            Arbitraries.strings().alpha().ofMinLength(2).ofMaxLength(20),
            Arbitraries.integers().between(18, 100)
        ).as((name, age) -> new User(name, age));
    }
}

```

`Combinators.combine()` prend en entrée plusieurs `Arbitrary` et les assemble via `.as()` pour produire un objet final. Chaque champ est généré indépendamment, ce qui maximise la diversité des combinaisons explorées.

Dans la classe de test, on expose ce générateur via `@Provide` :

```java
class UserPropertyTest {

    @Property
    void userAgeShouldBePositive(@ForAll("users") User user) {
        assertTrue(user.getAge() > 0);
    }

    @Provide
    Arbitrary<User> users() {
        return UserGenerator.users();
    }
}

```

Cette approche se compose très bien : si `User` contient lui-même des objets imbriqués (une adresse, un rôle, une liste de permissions) il suffit d'ajouter des arbitraires supplémentaires dans le `combine()`. Le générateur reste lisible et les responsabilités sont clairement séparées entre la logique de génération dans `UserGenerator` et son exposition dans la classe de test via `@Provide`.

## Conclusion

Les générateurs personnalisés sont ce qui fait passer les tests par propriété d'un outil expérimental à un véritable allié du quotidien. En produisant des données réalistes et cohérentes avec le métier, ils permettent d'explorer automatiquement des milliers de scénarios que l'on n'aurait jamais pensé à écrire à la main.

Nous avons vu trois approches complémentaires selon le niveau de réutilisabilité souhaité :

- `ArbitraryProvider` \+ SPI pour un générateur global, activé automatiquement via une annotation.
- `@Provide` \+ méthode statique pour un générateur ponctuel, déclaré directement dans la classe de test.
- `Combinators.combine()` pour assembler des objets complexes à partir de générateurs simples.

Ces patterns se composent et s'imbriquent : un `UserGenerator` peut très bien s'appuyer sur un `EmailGenerator` pour produire des utilisateurs avec des adresses email valides. C'est là toute la puissance de l'approche.

[plateforme de données - sfeir.dev - Le média incontournable pour les passionnés de tech et d’intelligence artificielleL’actualité des dernières avancées et tendances en technologies et en intelligence artificielle, décryptée avec acuité par des experts IT.![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/icon/plume-point-d7bdfb2d-af52-4d08-ab63-5b466dae8305.png)sfeir.dev - Le média incontournable pour les passionnés de tech et d'intelligence artificielleStephane AUBRY![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/thumbnail/LOGO_NOIR-1-d7992500-d573-4b8e-b9ae-190e64ea0282.png)](https://www.sfeir.dev/tag/plateforme-de-donnees/)