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ß.
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! –
+1 Dies sollte die akzeptierte Antwort sein. Neben den großartigen Design-Feedback sind die AutoFixture-Beispiele in dieser Antwort einfacher als meine. –
Vielen Dank für diese detaillierte Antwort! – oopbase