2013-07-25 14 views
6

Jak skonfigurować test deterministyczny, aby sprawdzić, czy pozycje na liście są zamówione?kolejność testowania jednostek pozycji na liście

Najpierw zrobiłem następujące:

public void SyncListContainsSortedItems(
    [Frozen] SyncItem[] expected, 
    SyncItemList sut) 
{ 
    Assert.Equal(expected.OrderBy(x => x.Key).First(), sut.First()); 
} 

Ale tak jak we wszystkich dobrych testów, po raz pierwszy spojrzał na niepowodzenie przed modyfikacją mojego kodu. Oczywiście udało się to, bo szczęście miało miejsce, a potem się nie powiodło. Moja początkowa porażka nie jest deterministyczna.

drugie zrobiłem następujący, myśląc: „na pewno to zagwarantuje awarię”:

public void SyncListContainsSortedItems(
    [Frozen] SyncItem[] seed, 
    SyncItemList sut) 
{ 
    var expected = seed.OrderByDescending(x => x.Key); 
    Assert.Equal(expected.OrderBy(x => x.Key).First(), sut.First()); 
} 

ku mojemu zdziwieniu to również nie zapewni deterministycznego awarię. Zrozumiałem, że to dlatego, że zamrożone nasiona mogą być naturalnie tworzone w kolejności malejącej, więc naprawdę nie poprawiłem sytuacji.

W tej chwili moja implementacja nie zamawia elementów przechodzących przez konstruktor. Jak ustalić solidną linię podstawową do mojego testu?

Informacje dodatkowe Kod listy elementów synchronizacji pokazano poniżej. To nie dużo, ponieważ jest to projekt mam zwiedzania:

public class SyncItemList : List<SyncItem> 
{ 
    public SyncItemList(SyncItem[] input) 
    { 
     foreach (var item in input) { this.Add(item); } 
    } 
} 

Aktualizacja Mam rozwija test. Następująca działa, ale z dużą szczegółowością.

public void SyncListContainsSortedItems(IFixture fixture, List<SyncItem> seed) 
{ 
    var seconditem = seed.OrderBy(x => x.Key).Skip(1).First(); 
    seed.Remove(seconditem); 
    seed.Insert(0, seconditem); 
    var seedArray = seed.ToArray(); 

    var ascending = seedArray.OrderBy(x => x.Key).ToArray(); 
    var descending = seedArray.OrderByDescending(x => x.Key).ToArray(); 
    Assert.NotEqual(ascending, seedArray); 
    Assert.NotEqual(descending, seedArray); 

    fixture.Inject<SyncItem[]>(seedArray); 
    var sut = fixture.Create<SyncItemList>(); 

    var expected = ascending; 
    var actual = sut.ToArray(); 
    Assert.Equal(expected, actual); 
} 

Jeden prosty sposób na zmianę mojego wdrożenia, aby przejść do dziedziczenia z SortedSet<SyncItem> zamiast List<SyncItem>.

+0

Jak wygląda "SyncItemList"? –

+0

Wiesz, co by było naprawdę fajne, gdyby istniał atrybut i typ żądania UnOrdered, przypominający skończoną sekwencję. Potrzebujesz tylko 3 lub więcej unikatowych przedmiotów i możesz zagwarantować sekwencję poza kolejnością. – cocogorilla

+0

Czy mogę zapytać, z której wersji AutoFixture korzystasz? [Liczby są losowe] (https://github.com/AutoFixture/AutoFixture/wiki/AutoFixture-3.0-Release-Notes#numbers-are-random) w AutoFixture 3. –

Odpowiedz

10

Jest na to wiele sposobów.

wersja Imperatyw

Oto prostsza wersja imperatyw niż dostarczony w PO:

[Fact] 
public void ImperativeTest() 
{ 
    var fixture = new Fixture(); 
    var expected = fixture.CreateMany<SyncItem>(3).OrderBy(si => si.Key); 
    var unorderedItems = expected.Skip(1).Concat(expected.Take(1)).ToArray(); 
    fixture.Inject(unorderedItems); 

    var sut = fixture.Create<SyncItemList>(); 

    Assert.Equal(expected, sut); 
} 

Podczas the default number of many items is 3, myślę, że lepiej zadzwonić go wyraźnie w tym przypadku testowego. Stosowany tutaj algorytm szyfrowania wykorzystuje fakt, że uporządkowanie sekwencji trzech (różnych) elementów, przesuwając pierwszy element do tyłu, musi skutkować nieuporządkowaną listą.

Jednak problem z tym podejściem polega na tym, że polega on na zmutowaniu modelu fixture, więc trudno go refaktoryzować, aby uzyskać bardziej deklaratywne podejście.

dostosowaną wersję

W celu byłaby do bardziej deklaratywny wersji, można najpierw hermetyzacji algorytm kodowania w a Customization:

public class UnorderedSyncItems : ICustomization 
{ 
    public void Customize(IFixture fixture) 
    { 
     fixture.Customizations.Add(new UnorderedSyncItemsGenerator()); 
    } 

    private class UnorderedSyncItemsGenerator : ISpecimenBuilder 
    { 
     public object Create(object request, ISpecimenContext context) 
     { 
      var t = request as Type; 
      if (t == null || 
       t != typeof(SyncItem[])) 
       return new NoSpecimen(request); 

      var items = ((IEnumerable)context 
       .Resolve(new FiniteSequenceRequest(typeof(SyncItem), 3))) 
       .Cast<SyncItem>(); 
      return items.Skip(1).Concat(items.Take(1)).ToArray(); 
     } 
    } 
} 

rozwiązywania new FiniteSequenceRequest(typeof(SyncItem), 3)) jest po prostu słabo wpisany (nie generyczny) sposób tworzenia skończonej sekwencji instancji SyncItem; to jest to, co robi CreateMany<SyncItem>(3) za kulisami.

Umożliwia to byłaby test do:

[Fact] 
public void ImperativeTestWithCustomization() 
{ 
    var fixture = new Fixture().Customize(new UnorderedSyncItems()); 
    var expected = fixture.Freeze<SyncItem[]>().OrderBy(si => si.Key); 

    var sut = fixture.Create<SyncItemList>(); 

    Assert.Equal(expected, sut); 
} 

Zauważ użycie metody Freeze. Jest to konieczne, ponieważ dostosowywanie UnorderedSyncItems zmienia tylko sposób tworzenia instancji; nadal tworzy nową tablicę za każdym razem, gdy otrzyma taką prośbę. Freeze zapewnia, że ​​ta sama tablica jest ponownie używana za każdym razem - także wtedy, gdy fixture tworzy instancję sut.

Konwencja oparte testowanie

Powyższy test może być refactored do deklaratywnej, test konwencja opartej przez wprowadzenie atrybutu [UnorderedConventions]:

public class UnorderedConventionsAttribute : AutoDataAttribute 
{ 
    public UnorderedConventionsAttribute() 
     : base(new Fixture().Customize(new UnorderedSyncItems())) 
    { 
    } 
} 

To jest po prostu deklaratywne klej do zastosowania dostosowania UnorderedSyncItems. Badanie staje się teraz:

[Theory, UnorderedConventions] 
public void ConventionBasedTest(
    [Frozen]SyncItem[] unorderedItems, 
    SyncItemList sut) 
{ 
    var expected = unorderedItems.OrderBy(si => si.Key); 
    Assert.Equal(expected, sut); 
} 

Wskazówka korzystanie z [UnorderedSyncItems] i [Frozen] atrybutów.

Ten test jest bardzo zwięzły, ale może nie być tym, na co masz ochotę. Problem polega na tym, że zmiana zachowania jest teraz ukryta w atrybucie [UnorderedSyncItems], więc raczej niejawne, co się dzieje. Ja wolę używać samego dostosowania jako zestawu konwencji dla całego zestawu testów, więc nie lubię wprowadzania odmian przypadków testowych na tym poziomie. Jeśli jednak twoje konwencje stwierdzą, że instancje powinny zawsze być nieautentyczne, to zawsze należy je anulować. Ta konwencja jest dobra.

Jeśli jednak chcesz tylko użyć nieuporządkowanych tablic dla niektórych przypadków testowych, użycie atrybutu [AutoData] nie jest najbardziej optymalnym podejściem.

deklaratywna sprawdzian

Byłoby miło, jeśli można po prostu zastosować atrybut parametr poziomu, podobnie jak atrybut [Frozen] - być może ich łączenie, jak [Unordered][Frozen]. Jednak to podejście nie działa.

Zawiadomienie z poprzednich przykładów, że zamówienie ma znaczenie. Przed zamrożeniem musisz zastosować UnorderedSyncItems, ponieważ w przeciwnym razie zamrożona tablica może nie być gwarantowana.

Problem z atrybutami poziomu parametru polega na tym, że podczas kompilacji struktura .NET nie gwarantuje kolejności atrybutów, gdy biblioteka kleju AutoFixture xUnit.net odczytuje i stosuje atrybuty.

Zamiast tego można zdefiniować pojedynczy atrybut stosuje się, podobnie jak to:

public class UnorderedFrozenAttribute : CustomizeAttribute 
{ 
    public override ICustomization GetCustomization(ParameterInfo parameter) 
    { 
     return new CompositeCustomization(    
      new UnorderedSyncItems(), 
      new FreezingCustomization(parameter.ParameterType)); 
    } 
} 

(FreezingCustomization zapewnia podstawową implementację atrybutu [Frozen]).

Pozwala to napisać ten test:

[Theory, AutoData] 
public void DeclarativeTest(
    [UnorderedFrozen]SyncItem[] unorderedItems, 
    SyncItemList sut) 
{ 
    var expected = unorderedItems.OrderBy(si => si.Key); 
    Assert.Equal(expected, sut); 
} 

Należy zauważyć, że ten test deklaratywny używa domyślnego atrybutu [AutoData] bez żadnych dostosowań. zacji, ponieważ szyfrowanie jest teraz stosowane przez atrybut [UnorderedFrozen] na poziomie parametru.

Umożliwi to również korzystanie z zestawu (innych) konwencji obejmujących cały pakiet testowy, zawartych w atrybucie [AutoData] i nadal będzie używać [UnorderedFrozen] jako mechanizmu akceptacji.

+2

Wow, Mark, tak doceniam inwestycję w tę odpowiedź. Progresja jest dla mnie bardzo cenna w zrozumieniu warstw autozapisu i tego, jak mogę zacząć je obierać, aby naprawdę uzyskać to, czego chcę w każdym scenariuszu, z którym mam do czynienia. Kudos i dziękuję! – cocogorilla

Powiązane problemy