Come implementare le restrizioni di data con AutoFixture?

Attualmente sto avendo una class modello che contiene diverse proprietà. Un modello semplificato potrebbe assomigliare a questo:

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

Ora sto implementando alcuni test unitari usando NUnit e uso AutoFixture per creare alcuni dati casuali:

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

Questo funziona perfettamente finora. Ma c’è il requisito che la data di ValidFrom sia sempre prima di ExpirationDate . Devo assicurarmi di ciò poiché sto implementando alcuni test positivi.

Quindi c’è un modo semplice per implementare questo utilizzando AutoFixture? So che potrei creare una data di correzione e aggiungere un intervallo di date casuali per risolvere questo problema, ma sarebbe bello se AutoFixture potesse gestire questo requisito stesso.

Non ho molta esperienza con AutoFixture, ma so che posso ottenere un ICustomizationComposer chiamando il metodo Build :

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

Forse questo è il modo giusto per raggiungere questo objective?

Grazie in anticipo per qualsiasi aiuto.

Potrebbe essere tentato di porre la domanda su come posso adattare AutoFixture al mio progetto? , ma spesso, una domanda più interessante potrebbe essere: come posso rendere il mio design più robusto?

È ansible mantenere il progetto e correggere l’effetto AutoFixture, ma non penso che sia un’idea particolarmente valida.

Prima di dirti come farlo, a seconda delle tue esigenze, forse tutto ciò che devi fare è il seguente.

Assegnazione esplicita

Perché non assegnare semplicemente un valore valido a ExpirationDate , come questo?

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

Se stai utilizzando AutoFixture.Xunit , può essere ancora più semplice:

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

Questo è abbastanza robusto, perché anche se AutoFixture (IIRC) crea valori TimeSpan casuali, rimarranno nell’intervallo positivo a meno che tu non abbia fatto qualcosa al tuo fixture per cambiarne il comportamento.

Questo approccio sarebbe il modo più semplice per rispondere alla domanda se è necessario testare SomeClass stesso. D’altra parte, non è molto pratico se hai bisogno di SomeClass come valori di input in una miriade di altri test.

In questi casi, può essere tentato di risolvere AutoFixture, che è anche ansible:

Modifica del comportamento di AutoFixture

Ora che hai visto come affrontare il problema come una soluzione una tantum, puoi dire a AutoFixture come una modifica generale del modo in cui SomeClass viene generato:

 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 classs, as // long as the fixture instance is the same... var sc = fixture.Create(); 

Puoi anche impacchettare la chiamata sopra per Customize in un’implementazione di personalizzazione, per un ulteriore riutilizzo. Ciò consentirebbe anche di utilizzare un’istanza Fixture personalizzata con AutoFixture.Xunit.

Cambia il design del SUT

Mentre le soluzioni sopra descritte descrivono come modificare il comportamento di AutoFixture, AutoFixture è stato originariamente scritto come strumento TDD e il punto principale di TDD è quello di fornire un feedback sul Sistema sotto test (SUT). AutoFixture tende ad amplificare quel tipo di feedback, che è anche il caso qui.

Considera il design di SomeClass . Nulla impedisce a un client di fare qualcosa del genere:

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

Questo compila e funziona senza errori, ma probabilmente non è quello che vuoi. In questo modo, AutoFixture in realtà non sta facendo nulla di sbagliato; SomeClass non protegge adeguatamente i suoi invarianti.

Questo è un errore di progettazione comune, in cui gli sviluppatori tendono a mettere troppa fiducia nelle informazioni semantiche dei nomi dei membri. Il pensiero sembra essere che nessuno ValidFrom mente avrebbe impostato ExpirationDate su un valore prima di ValidFrom ! Il problema con questo tipo di argomento è che presuppone che tutti gli sviluppatori assegneranno questi valori in coppia.

Tuttavia, i client possono anche ricevere un’istanza SomeClass e desiderano aggiornare uno dei valori, ad esempio:

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

È valido? Come puoi dirlo?

Il client potrebbe guardare sc.ValidFrom , ma perché dovrebbe? Lo scopo dell’intero incapsulamento è di sollevare i clienti da tali oneri.

Invece, dovresti considerare di cambiare il design SomeClass . Il più piccolo cambiamento di design che riesco a pensare è qualcosa del genere:

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

Questo trasforma ExpirationDate in una proprietà calcasting di sola lettura . Con questa modifica, AutoFixture funziona appena fuori dalla scatola:

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

Puoi anche usarlo con AutoFixture.Xunit:

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

Questo è ancora un po ‘ fragile, perché sebbene, per impostazione predefinita, AutoFixture crei valori TimeSpan positivi, è ansible modificare anche tale comportamento.

Inoltre, il design consente ai clienti di assegnare valori TimeSpan negativi alla proprietà Duration :

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

Indipendentemente dal fatto che questo debba o meno essere consentito dipende dal modello di dominio. Una volta che si inizia a considerare questa possibilità, potrebbe effettivamente risultare che la definizione dei periodi temporali che si spostano indietro nel tempo è valida nel dominio …

Design secondo la legge di Postel

Se il dominio del problema è quello in cui tornare indietro nel tempo non è consentito, potresti prendere in considerazione l’aggiunta di una clausola di guardia alla proprietà Duration , rifiutando gli intervalli di tempo negativi.

Tuttavia, personalmente, trovo spesso che ottengo un design API migliore quando prendo sul serio la legge di Postel . In questo caso, perché non modificare la progettazione in modo che SomeClass utilizzi sempre TimeSpan assoluto anziché TimeSpan firmato?

In tal caso, preferirei un object immutabile che non imponga i ruoli di due istanze DateTime finché non ne conosce i valori:

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

Come la precedente riprogettazione, questo funziona appena fuori dalla scatola con AutoFixture:

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

La situazione è la stessa con AutoFixture.Xunit, ma ora nessun client può configurarlo erroneamente.

Che tu trovi o meno un progetto del genere, spetta a te, ma spero che almeno sia uno spunto di riflessione.

Questa è una sorta di “commento esteso” in riferimento alla risposta di Mark, cercando di build sulla sua soluzione di Postel’s Law. Lo scambio dei parametri nel costruttore mi è sembrato a disagio, quindi ho reso esplicito il comportamento dello scambio di date in una class Period.

Usando syntax C # 6 per brevità :

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

Dovresti quindi personalizzare il tuo dispositivo per Period per utilizzare il costruttore statico:

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

Penso che il principale vantaggio di questa soluzione sia che estrae il requisito di ordinazione dei tempi nella propria class (SRP) e lascia la logica aziendale espressa in termini di un contratto già concordato, evidente dalla firma del costruttore.

Poiché SomeClass è modificabile, ecco un modo per farlo:

 [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, usando AutoFixture con teorie dei dati xUnit.net , il test precedente può essere scritto come:

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