2015-02-19 3 views
12

Ich habe derzeit eine Modellklasse, die mehrere Eigenschaften enthält. Ein vereinfachtes Modell könnte wie folgt aussehen:Wie werden Datumsbeschränkungen mit AutoFixture implementiert?

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

nun unter Verwendung NUnit und verwenden AutoFixture einige Unit-Tests ich Implementierung einige zufällige Daten zu erstellen:

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

Das bisher perfekt funktioniert. Aber es gibt die Anforderung, dass das Datum ValidFromimmer vorExpirationDate ist. Ich muss dies sicherstellen, da ich einige positive Tests durchführe.

So gibt es eine einfache Möglichkeit, dies zu implementieren, indem Sie AutoFixture? Ich weiß, dass ich ein Fixedatum erstellen und ein zufälliges Datumintervall hinzufügen könnte, um dieses Problem zu lösen, aber es wäre großartig, wenn AutoFixture diese Anforderung selbst bewältigen könnte.

Ich habe viel Erfahrung mit AutoFixture nicht bekommen, aber ich weiß, dass ich ein ICustomizationComposer durch den Aufruf der Methode Build bekommen:

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

Vielleicht ist dies der richtige Weg, dies zu erreichen?

Vielen Dank im Voraus für jede Hilfe.

Antwort

26

Es kann verlockend sein, um die Frage von zu stellen, wie mache ich AutoFixture an mein Design anpassen?, aber oft könnte eine interessantere Frage sein: Wie mache ich mein Design robuster?

Sie können das Design beibehalten und AutoFixture "reparieren", aber ich denke nicht, dass es eine besonders gute Idee ist.

Bevor ich Ihnen sagen, wie das zu tun ist, abhängig von Ihren Anforderungen, ist alles, was Sie tun müssen, das Folgende.

Explizite Zuordnung

Warum nicht einfach einen gültigen Wert ExpirationDate zuweisen, wie das?

var sc = fixture.Create<SomeClass>(); 
sc.ExpirationDate = sc.ValidFrom + fixture.Create<TimeSpan>(); 

// Perform test here... 

Wenn Sie AutoFixture.Xunit verwenden, kann es sein, noch einfacher:

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

    // Perform test here... 
} 

Das ist ziemlich robust, denn obwohl AutoFixture (IIRC) erzeugt zufällige TimeSpan Werte, sie in die bleiben werde positiver Bereich, es sei denn, Sie haben etwas an Ihrem fixture getan, um sein Verhalten zu ändern.

Dieser Ansatz wäre der einfachste Weg, um Ihre Frage zu beantworten, wenn Sie SomeClass selbst testen müssen. Auf der anderen Seite ist es nicht sehr praktisch, wenn Sie in unzähligen anderen Tests SomeClass als Eingabewerte benötigen.

In solchen Fällen kann es verlockend sein, AutoFixture zu beheben, was auch möglich ist:

Ändern AutoFixture Verhalten

Nachdem Sie nun gesehen haben, wie das Problem als eine einmalige Adresse Lösung können Sie AutoFixture darüber als eine allgemeine Änderung der Art und Weise sagen SomeClass erzeugt wird:

fixture.Customize<SomeClass>(c => c 
    .Without(x => x.ValidFrom) 
    .Without(x => x.ExpirationDate) 
    .Do(x => 
     { 
      x.ValidFrom = fixture.Create<DateTime>(); 
      x.ExpirationDate = 
       x.ValidFrom + fixture.Create<TimeSpan>(); 
     })); 
// 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<SomeClass>(); 

Sie können auch die oben Aufruf Customize Paket in eine ICustomization Implementierung, für die weitere Wiederverwendung. Dadurch können Sie auch eine benutzerdefinierte Fixture Instanz mit AutoFixture.Xunit verwenden.

Ändern Sie den Entwurf des SUT

Während die oben genannten Lösungen beschreiben, wie das Verhalten von AutoFixture zu ändern, AutoFixture ursprünglich als TDD-Tool geschrieben wurde, und der wichtigste Punkt der TDD auf Rückmeldungen über die System unter Test (SUT). AutoFixture neigt dazu, diese Art von Feedback zu verstärken,, was auch hier der Fall ist.

Betrachten Sie das Design von SomeClass. Nichts hindert einen Client von so etwas wie dies zu tun:

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

Dies kompiliert und läuft ohne Fehler, aber es ist wahrscheinlich nicht das, was Sie wollen. Somit macht AutoFixture tatsächlich nichts falsch; SomeClass schützt seine Invarianten nicht richtig.

Dies ist ein häufiger Konstruktionsfehler, bei dem Entwickler dazu neigen, zu viel Vertrauen in die semantische Information der Namen der Mitglieder zu setzen. Das Denken scheint zu sein, dass niemand in ihrer rechten Meinung ExpirationDate auf einen Wert vorValidFrom setzen würde! Das Problem mit dieser Art von Argument ist, dass angenommen wird, dass alle Entwickler diese Werte immer paarweise zuweisen werden.

jedoch Kunden erhalten auch eine SomeClass Instanz an sie übergeben, und wollen einen der Werte aktualisieren, z.B .:

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

Ist das gültig? Woran erkennst du das?

Der Client konnte sc.ValidFrom betrachten, aber warum sollte es? Das ganze Zweck von Verkapselung soll Kunden solcher Belastungen entlasten.

Stattdessen sollten Sie das Design SomeClass ändern. Die kleinste Designänderung ich denken kann, ist dies so etwas wie:

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

Das macht ExpirationDate in eine Nur-Lese-, berechnete Eigenschaft.Mit dieser Änderung AutoFixture funktioniert einfach aus der Box:

var sc = fixture.Create<SomeClass>(); 

// Perform test here... 

Sie auch mit AutoFixture.Xunit verwenden können:

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

Dies ist immer noch ein wenig spröde, denn obwohl standardmäßig AutoFixture erzeugt positive TimeSpan Werte, es ist auch möglich, dieses Verhalten zu ändern.

Darüber hinaus ist die Konstruktion tatsächlich erlaubt es den Kunden negativ TimeSpan Werte an die Duration Eigenschaft zuweisen:

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

Ob dies erlaubt sein sollte, bis zu dem Domain Model ist. Sobald Sie anfangen, diese Möglichkeit zu prüfen, kann es tatsächlich herausstellen, dass Zeitperioden definieren, die in der Zeit rückwärts bewegen sich in der Domain gültig ist ...

Ausführung nach Postel Gesetz

Wenn das Problem Domain eine, wo zurück in der Zeit nicht erlaubt ist, können Sie könnte in Betracht ziehen, eine Guard Clause der Duration Eigenschaft hinzuzufügen, negative Zeitspannen abzulehnen.

Allerdings finde ich oft, dass ich zu einem besseren API-Design komme, wenn ich Postel's Law ernst nehme. In diesem Fall, warum nicht das Design ändern, so dass SomeClass immer die absolute TimeSpan anstelle der signierten TimeSpan verwendet?

In diesem Fall würde ich ein unveränderliches Objekt bevorzugen, die nicht die Rollen von zwei DateTime Fällen erzwingen, bis sie ihre Werte kennen:

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

Wie die vorherige Redesign, das funktioniert nur out mit AutoFixture der Box:

var sc = fixture.Create<SomeClass>(); 

// Perform test here... 

die Situation ist das gleiche mit AutoFixture.Xunit, aber jetzt keine Kunden falsch konfigurieren können.

Ob Sie ein solches Design passend finden oder nicht, liegt an Ihnen, aber ich hoffe, es ist zumindest Denkanstoß.

+3

Dies ist eine gute Antwort. Um hinzuzufügen, nehme ich an, dass es möglich ist, dass SomeClass tatsächlich * behavior * hat, das diese setzt, zum Beispiel Activate() oder Expire() Methoden. Vielleicht brauchen diese Eigenschaften nicht einmal Setter! –

+0

+1 Dies sollte die akzeptierte Antwort sein. Neben den großartigen Design-Feedback sind die AutoFixture-Beispiele in dieser Antwort einfacher als meine. –

+0

Vielen Dank für diese detaillierte Antwort! – oopbase

3

Seit SomeClass ist wandelbar, hier ist ein Weg, es zu tun:

[Fact] 
public void UsingGeneratorOfDateTime() 
{ 
    var fixture = new Fixture(); 
    var generator = fixture.Create<Generator<DateTime>>(); 
    var sut = fixture.Create<SomeClass>(); 
    var seed = fixture.Create<int>(); 

    sut.ExpirationDate = 
     generator.First().AddYears(seed); 
    sut.ValidFrom = 
     generator.TakeWhile(dt => dt < sut.ExpirationDate).First(); 

    Assert.True(sut.ValidFrom < sut.ExpirationDate); 
} 

FWIW, mit AutoFixture with xUnit.net data theories kann der obige Test geschrieben werden als:

[Theory, AutoData] 
public void UsingGeneratorOfDateTimeDeclaratively(
    Generator<DateTime> 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); 
} 
4

Dies ist eine Art "erweiterter Kommentar" in Bezug auf Marks Antwort und versucht, auf seiner Postel's Law-Lösung aufzubauen. Der Parameter-Swapping im Konstruktor fühlte sich für mich unbehaglich an, daher habe ich das Datumswechsel-Verhalten in einer Periode-Klasse explizit gemacht.

Mit C#6 syntax der Kürze:

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

Sie würden dann benötigen eine Halterung anpassen für Period den statischen Konstruktor zu verwenden:

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

Ich denke, der Hauptvorteil dieser Lösung ist, dass es die Zeitordnungsanforderung in seine eigene Klasse (SRP) extrahiert und es Ihrer Geschäftslogik erlaubt, in einem bereits vereinbarten Vertrag ausgedrückt zu werden, der aus dem Konstrukt ersichtlich ist r Unterschrift.