Comment implémenter des ressortingctions de date avec AutoFixture?

J’ai actuellement une classe de modèle qui contient plusieurs propriétés. Un modèle simplifié pourrait ressembler à ceci:

public class SomeClass { public DateTime ValidFrom { get; set; } public DateTime ExpirationDate { get; set; } } 

Maintenant, j’implémente des tests unitaires en utilisant NUnit et j’utilise AutoFixture pour créer des données aléatoires:

 [Test] public void SomeTest() { var fixture = new Fixture(); var someRandom = fixture.Create(); } 

Cela fonctionne parfaitement jusqu’à présent. Mais il faut que la date de ValidFrom soit toujours antérieure à ExpirationDate . Je dois m’assurer de cela car je mets en place des tests positifs.

Existe-t-il donc un moyen simple d’implémenter cela en utilisant AutoFixture? Je sais que je pourrais créer une date fixe et append un intervalle de dates aléatoire pour résoudre ce problème, mais ce serait bien si AutoFixture pouvait gérer cette exigence lui-même.

Je n’ai pas beaucoup d’expérience avec AutoFixture, mais je sais que je peux obtenir un ICustomizationComposer en appelant la méthode Build :

 var fixture = new Fixture(); var someRandom = fixture.Build() .With(some => /*some magic like some.ValidFrom < some.ExpirationDate here...*/ ) .Create(); 

Peut-être que c’est la bonne façon d’y parvenir?

Merci d’avance pour votre aide.

    Il peut être tentant de se demander comment adapter AutoFixture à ma conception. , mais souvent, une question plus intéressante pourrait être: comment rendre mon design plus robuste?

    Vous pouvez conserver la conception et «corriger» AutoFixture, mais je ne pense pas que ce soit une très bonne idée.

    Avant de vous expliquer comment procéder, selon vos besoins, il vous suffira peut-être de procéder comme suit.

    Affectation explicite

    Pourquoi ne pas simplement assigner une valeur valide à ExpirationDate , comme ceci?

     var sc = fixture.Create(); sc.ExpirationDate = sc.ValidFrom + fixture.Create(); // Perform test here... 

    Si vous utilisez AutoFixture.Xunit , cela peut être encore plus simple:

     [Theory, AutoData] public void ExplicitPostCreationFix_xunit( SomeClass sc, TimeSpan duration) { sc.ExpirationDate = sc.ValidFrom + duration; // Perform test here... } 

    C’est assez robuste, car même si AutoFixture (IIRC) crée des valeurs TimeSpan aléatoires, elles restront dans la plage positive à moins que vous n’ayez fait quelque chose à votre fixture pour changer son comportement.

    Cette approche serait le moyen le plus simple de répondre à votre question si vous devez tester SomeClass lui-même. D’autre part, ce n’est pas très pratique si vous avez besoin de SomeClass comme valeur d’entrée dans des myriades d’autres tests.

    Dans de tels cas, il peut être tentant de corriger AutoFixture, ce qui est également possible:

    Changer le comportement de AutoFixture

    Maintenant que vous avez compris comment résoudre le problème en tant que solution unique, vous pouvez en parler à AutoFixture en tant que modification générale de la façon dont SomeClass est généré:

     fixture.Customize(c => c .Without(x => x.ValidFrom) .Without(x => x.ExpirationDate) .Do(x => { x.ValidFrom = fixture.Create(); x.ExpirationDate = x.ValidFrom + fixture.Create(); })); // All sorts of other things can happen in between, and the // statements above and below can happen in separate classes, as // long as the fixture instance is the same... var sc = fixture.Create(); 

    Vous pouvez également conditionner l’appel ci-dessus à Customize dans une implémentation ICustomization , pour une réutilisation ultérieure. Cela vous permettrait également d’utiliser une instance de Fixture personnalisée avec AutoFixture.Xunit.

    Changer la conception du SUT

    Alors que les solutions ci-dessus décrivent comment modifier le comportement d’AutoFixture, AutoFixture était à l’origine un outil TDD, et l’objective principal de TDD est de fournir des commentaires sur le système à tester. La correction automatique a tendance à amplifier ce type de retour, ce qui est également le cas ici.

    Considérez le design de SomeClass . Rien n’empêche un client de faire quelque chose comme ça:

     var sc = new SomeClass { ValidFrom = new DateTime(2015, 2, 20), ExpirationDate = new DateTime(1900, 1, 1) }; 

    Cela comstack et fonctionne sans erreur, mais ce n’est probablement pas ce que vous voulez. Ainsi, AutoFixture ne fait rien de mal; SomeClass ne protège pas correctement ses invariants.

    Il s’agit d’une erreur de conception courante, dans laquelle les développeurs ont tendance à trop se fier aux informations sémantiques des noms des membres. La pensée semble être que personne de ValidFrom ne définirait ExpirationDate sur une valeur avant ValidFrom ! Le problème avec ce type d’argument est qu’il suppose que tous les développeurs assigneront toujours ces valeurs par paires.

    Cependant, les clients peuvent également se voir transmettre une instance de SomeClass et souhaiter mettre à jour l’une des valeurs, par exemple:

     sc.ExpirationDate = new DateTime(2015, 1, 31); 

    Est-ce valide? Comment pouvez-vous dire?

    Le client peut consulter sc.ValidFrom , mais pourquoi le devrait-il? Le but de l’ encapsulation est de soulager les clients de telles charges.

    Au lieu de cela, vous devriez envisager de modifier la conception SomeClass . Le plus petit changement de conception auquel je puisse penser ressemble à ceci:

     public class SomeClass { public DateTime ValidFrom { get; set; } public TimeSpan Duration { get; set; } public DateTime ExpirationDate { get { return this.ValidFrom + this.Duration; } } } 

    Cela transforme ExpirationDate en une propriété calculée en lecture seule . Avec cette modification, AutoFixture fonctionne sans aucun problème:

     var sc = fixture.Create(); // Perform test here... 

    Vous pouvez également l’utiliser avec AutoFixture.Xunit:

     [Theory, AutoData] public void ItJustWorksWithAutoFixture_xunit(SomeClass sc) { // Perform test here... } 

    Cela rest un peu fragile, car bien que, par défaut, AutoFixture crée des valeurs TimeSpan positives, il est également possible de modifier ce comportement.

    En outre, la conception permet en réalité aux clients d’atsortingbuer des valeurs TimeSpan négatives à la propriété Duration :

     sc.Duration = TimeSpan.FromHours(-1); 

    Que cela soit autorisé ou non dépend du modèle de domaine. Une fois que vous commencez à envisager cette possibilité, il peut s’avérer que définir des périodes qui reculent dans le temps est valide dans le domaine …

    Conception conforme à la loi de Postel

    Si le domaine problématique ne permet pas de remonter dans le temps, vous pouvez envisager d’append une clause de garde à la propriété Duration , en rejetant les intervalles de temps négatifs.

    Cependant, personnellement, je trouve souvent que je parviens à une meilleure conception d’API lorsque je prends la loi de Postel au sérieux. Dans ce cas, pourquoi ne pas modifier la conception de sorte que SomeClass utilise toujours TimeSpan absolu au lieu du TimeSpan signé?

    Dans ce cas, je préférerais un object immuable qui n’impose pas les rôles de deux instances DateTime tant qu’il ne connaît pas leurs valeurs:

     public class SomeClass { private readonly DateTime validFrom; private readonly DateTime expirationDate; public SomeClass(DateTime x, DateTime y) { if (x < y) { this.validFrom = x; this.expirationDate = y; } else { this.validFrom = y; this.expirationDate = x; } } public DateTime ValidFrom { get { return this.validFrom; } } public DateTime ExpirationDate { get { return this.expirationDate; } } } 

    Comme pour la refonte précédente, cela fonctionne immédiatement avec AutoFixture:

     var sc = fixture.Create(); // Perform test here... 

    La situation est la même avec AutoFixture.Xunit, mais aucun client ne peut désormais le configurer de manière incorrecte.

    Que vous trouviez ou non un tel design approprié dépend de vous, mais j'espère au moins que cela donne à réfléchir.

    Il s’agit d’une sorte de “commentaire étendu” faisant référence à la réponse de Mark, qui tente de s’appuyer sur la solution de Postel. L’échange de parameters dans le constructeur m’inquiétait, j’ai donc explicité le comportement d’échange de dates dans une classe Period.

    Utilisation de la syntaxe C # 6 pour la brièveté :

     public class Period { public DateTime Start { get; } public DateTime End { get; } public Period(DateTime start, DateTime end) { if (start > end) throw new ArgumentException("start should be before end"); Start = start; End = end; } public static Period CreateSpanningDates(DateTime x, DateTime y, params DateTime[] others) { var all = others.Concat(new[] { x, y }); var start = all.Min(); var end = all.Max(); return new Duration(start, end); } } public class SomeClass { public DateTime ValidFrom { get; } public DateTime ExpirationDate { get; } public SomeClass(Period period) { ValidFrom = period.Start; ExpirationDate = period.End; } } 

    Vous devrez ensuite personnaliser votre fixture pour Period afin d’utiliser le constructeur statique:

     fixture.Customize(f => f.FromFactory((x, y) => Period.CreateSpanningDates(x, y))); 

    Je pense que le principal avantage de cette solution est qu’elle extrait l’exigence de commande dans sa propre classe (SRP) et laisse votre logique d’affaires être exprimée en termes de contrat déjà conclu, tel qu’il ressort de la signature du constructeur.

    Puisque SomeClass est modifiable, voici une façon de le faire:

     [Fact] public void UsingGeneratorOfDateTime() { var fixture = new Fixture(); var generator = fixture.Create>(); var sut = fixture.Create(); var seed = fixture.Create(); sut.ExpirationDate = generator.First().AddYears(seed); sut.ValidFrom = generator.TakeWhile(dt => dt < sut.ExpirationDate).First(); Assert.True(sut.ValidFrom < sut.ExpirationDate); } 

    FWIW, en utilisant AutoFixture avec les théories de données xUnit.net , le test ci-dessus peut être écrit comme suit :

     [Theory, AutoData] public void UsingGeneratorOfDateTimeDeclaratively( Generator generator, SomeClass sut, int seed) { sut.ExpirationDate = generator.First().AddYears(seed); sut.ValidFrom = generator.TakeWhile(dt => dt < sut.ExpirationDate).First(); Assert.True(sut.ValidFrom < sut.ExpirationDate); }