JustDummies

Juste des dummies, mais redoutablement efficaces.

Sections de la documentation

Composition

Les générateurs fournis couvrent les primitifs. Votre code, lui, est fait de références de commande, de montants, de clients et d’agrégats. Cette page traite du franchissement de cet écart : transformer des primitifs contraints en dummies pour vos types, sans jamais produire une valeur que votre propre constructeur refuserait.

.As(...) : d’un primitif vers votre type

Un objet-valeur enveloppe généralement un primitif derrière une fabrique qui valide. Contraignez le primitif pour qu’il satisfasse la fabrique, puis passez la fabrique à .As(...) :

// OrderReference.Create exige le préfixe « ORD- » et une longueur de 12. Les contraintes
// sont choisies pour que toute chaîne tirée franchisse cette barre — jamais pour faire
// passer une assertion.
IAny<OrderReference> anyReference = Any.String()
                                       .StartingWith("ORD-")
                                       .WithLength(12)
                                       .As(OrderReference.Create);

OrderReference reference = anyReference.Generate();

.As(...) prend un IAny<TSource> et un Func<TSource, TResult> et renvoie un IAny<TResult> — un générateur comme un autre, que l’on peut stocker, faire circuler, placer dans une collection ou rendre nullable.

C’est la voie prévue vers un type au contrat plus strict, et elle a une propriété qui mérite d’être nommée : la fabrique est la vraie. Si les contraintes sont trop lâches, la fabrique lève une exception, et vous l’apprenez immédiatement au lieu de livrer un dummy qui n’aurait jamais pu exister en production.

Any.Combine : plusieurs générateurs en un seul

Quand un type demande plus d’une entrée, Any.Combine tire de chaque générateur et alimente un composeur :

Comment Any.Combine compose deux générateurs en un seulUn générateur de decimal borné entre 0 et 1000 et un choix parmi EUR, USD et GBP sont composés en un seul IAny de Money, qui tire un Money tel que 412,75 EUR.Any.Decimal()Between(0, 1000)composerAny.OneOf(EUR, USD, GBP)IAny<Money>Money412,75 EUR

IAny<Money> anyMoney = Any.Combine(
    Any.Decimal().Between(0m, 1_000m).WithScale(2),
    Any.OneOf("EUR", "USD", "GBP"),
    Money.Create);

Money price = anyMoney.Generate();

Le composeur peut être un groupe de méthodes, comme ci-dessus, ou une lambda quand la forme demande un ajustement. Des surcharges existent de deux à huit générateurs.

Chaque opérande doit être réellement utilisé par le composeur. Un opérande tiré puis jeté est presque toujours une erreur — un paramètre resté non lu après un remaniement — d’où le diagnostic JD027. Quand le tirage est vraiment délibéré, nommez le paramètre _ pour le dire.

Quand huit ne suffit pas

L’arité s’arrête à huit volontairement (ADR-0005 ouvre un nouvel onglet). Un type réclamant plus de huit entrées indépendantes est un type qui appelle une structure intermédiaire, et composer cette structure est à la fois le contournement et la meilleure conception :

// Composez d'abord les parties...
IAny<Money>          anyPrice     = Any.Combine(Any.Decimal().Between(0m, 1_000m).WithScale(2),
                                                Any.OneOf("EUR", "USD", "GBP"),
                                                Money.Create);
IAny<OrderReference> anyReference = Any.String().StartingWith("ORD-").WithLength(12).As(OrderReference.Create);

// ...puis combinez les parties, non les primitifs.
IAny<string> anySummary = Any.Combine(
    anyReference,
    anyPrice,
    Any.Enum<OrderStatus>(),
    (orderRef, price, status) => $"{orderRef} — {price} — {status}");

Un générateur composé est un IAny<T> ordinaire : il alimente un autre Combine, une collection ou un .As(...) exactement comme un générateur primitif. C’est ce qui fait du plafond une contrainte de forme plutôt qu’une limite.

Any.PairOf et Any.TripleOf

Quand seul le tuple vous intéresse et qu’aucun composeur n’apporterait quoi que ce soit, deux raccourcis existent :

IAny<(int Quantity, decimal UnitPrice)> anyLine = Any.PairOf(
    Any.Int32().Between(1, 100),
    Any.Decimal().Between(0.01m, 500m).WithScale(2));

(int quantity, decimal unitPrice) = anyLine.Generate();

IAny<(Guid, string, OrderStatus)> anyRow = Any.TripleOf(
    Any.Guid().NonEmpty(),
    Any.String().Alpha().WithLengthBetween(3, 20),
    Any.Enum<OrderStatus>());

.OrNull() : les valeurs optionnelles

Un champ optionnel mérite un dummy parfois absent — sinon la branche nulle n’est jamais exercée. .OrNull() produit null environ une fois sur deux et, sinon, une valeur satisfaisant tout ce qui a été déclaré en amont :

// Types valeur : int?, DateTime?, Guid?, une énumération...
int?      discount  = Any.Int32().Between(0, 100).OrNull().Generate();
DateTime? cancelled = Any.DateTime().Before(new DateTime(2030, 1, 1)).OrNull().Generate();

// Types référence : une chaîne nullable, ou un objet-valeur construit via .As(...)
string?         note      = Any.String().Alpha().WithLengthBetween(1, 40).OrNull().Generate();
OrderReference? reference = Any.String().StartingWith("ORD-").WithLength(12)
                               .As(OrderReference.Create)
                               .OrNull()
                               .Generate();

Deux classes d’extension se cachent derrière cette écriture unique — NullableExtensions pour les types valeur et NullableReferenceExtensions pour les types référence — car une surcharge contrainte à struct et une autre à class entreraient en collision. Vous ne choisissez jamais entre elles : le compilateur le fait, d’après le type que vous générez.

La décision « null ou valeur » tire du même contexte aléatoire que le générateur enveloppé : une exécution graînée la rejoue donc exactement. Un tirage null ne consomme pas de valeur du générateur enveloppé.

.AsNullable() : un type nullable, jamais une valeur absente

L’opposé de .OrNull(), et celui dont vous avez besoin bien plus souvent que le nom ne le laisse croire. Un paramètre écrit OrderStatus? doit quand même recevoir une valeur ; si le test se moque de laquelle, le dummy qui lui convient n’est pas parfois-absent — un dummy absent exerce une branche que le test n’a jamais demandée. .AsNullable() élargit le type et laisse les valeurs tranquilles :

OrderStatus? status = Any.Enum<OrderStatus>().AsNullable().Generate();   // jamais null
int?         units  = Any.Int32().Between(1, 10).AsNullable().Generate();

Ça compte surtout à l’intérieur d’une collection distincte. .As(value => (OrderStatus?)value) dirait la même chose du type et rien du tout du domaine : un ensemble ne saurait donc pas dans combien de valeurs distinctes il a le droit de puiser, et en demanderait plus qu’il n’en existe.

// L'énum a un nombre de membres fixe, donc un ensemble en contient au plus autant — et ceci le sait.
ISet<OrderStatus?> statuses = Any.SetOf(Any.Enum<OrderStatus>().AsNullable()).NonEmpty().Generate();

Un générateur scaffoldé par dum écrit .AsNullable() pour chaque paramètre nullable de type valeur, exactement pour cette raison.

Construire un agrégat entier

En rassemblant tout, voici un dummy pour un enregistrement à trois champs, dont aucun n’est un primitif nu sur le site d’appel :

IAny<Customer> anyCustomer = Any.Combine(
    Any.Guid().NonEmpty(),
    Any.String().Alpha().WithLengthBetween(3, 20),
    Any.String().Alpha().InLowerCase().WithLengthBetween(3, 12),
    (id, name, localPart) => new Customer(id, name, $"{localPart}@example.test"));

Customer customer = anyCustomer.Generate();

// Un générateur est une recette : le même produit donc toute une liste de clients distincts.
List<Customer> customers = Any.ListOf(anyCustomer).WithCountBetween(2, 5).Generate();

Conservez un tel générateur dans un champ static readonly de votre classe de test et chaque test du fichier obtient un client valide en un appel — sans état mutable partagé, puisque les générateurs sont immuables.

Lire la source, ou la corriger là-bas ouvre un nouvel onglet· Repris depuis lib-v1.0.0-preview.6