Czy powtórzenie kodu dla testów jednostkowych jest w porządku?

11

Napisałem kilka algorytmów sortowania dla przypisania klasy, a także napisałem kilka testów, aby upewnić się, że algorytmy zostały poprawnie zaimplementowane. Moje testy mają tylko 10 linii i są 3 z nich, ale tylko 1 linia zmienia się między 3, więc jest dużo powtarzającego się kodu. Czy lepiej przekodować ten kod na inną metodę, która jest następnie wywoływana z każdego testu? Czy nie musiałbym wtedy pisać kolejnego testu, aby przetestować refaktoryzację? Niektóre zmienne można nawet przenieść na poziom klasy. Czy klasy i metody testowania powinny podlegać tym samym zasadom, co zwykłe klasy / metody?

Oto przykład:

    [TestMethod]
    public void MergeSortAssertArrayIsSorted()
    {
        int[] a = new int[1000];
        Random rand = new Random(DateTime.Now.Millisecond);
        for(int i = 0; i < a.Length; i++)
        {
            a[i] = rand.Next(Int16.MaxValue);
        }
        int[] b = new int[1000];
        a.CopyTo(b, 0);
        List<int> temp = b.ToList();
        temp.Sort();
        b = temp.ToArray();

        MergeSort merge = new MergeSort();
        merge.mergeSort(a, 0, a.Length - 1);
        CollectionAssert.AreEqual(a, b);
    }
    [TestMethod]
    public void InsertionSortAssertArrayIsSorted()
    {
        int[] a = new int[1000];
        Random rand = new Random(DateTime.Now.Millisecond);
        for (int i = 0; i < a.Length; i++)
        {
            a[i] = rand.Next(Int16.MaxValue);
        }
        int[] b = new int[1000];
        a.CopyTo(b, 0);
        List<int> temp = b.ToList();
        temp.Sort();
        b = temp.ToArray();

        InsertionSort merge = new InsertionSort();
        merge.insertionSort(a);
        CollectionAssert.AreEqual(a, b); 
    }
Pete
źródło

Odpowiedzi:

21

Kod testowy jest nadal kodem i również musi być utrzymywany.

Jeśli musisz zmienić skopiowaną logikę, musisz to zrobić w każdym miejscu, do którego ją skopiowałeś.

SUCHO nadal obowiązuje.

Czy nie musiałbym wtedy pisać kolejnego testu, aby przetestować refaktoryzację?

Czy ty A skąd wiesz, że testy, które obecnie masz, są poprawne?

Refaktoryzacja jest testowana przez uruchomienie testów. Wszystkie powinny mieć takie same wyniki.

Oded
źródło
Tak jest. Testy są kodem - nadal obowiązują te same zasady pisania dobrego kodu! Przetestuj refaktoryzację, uruchamiając testy, ale upewnij się, że istnieje odpowiedni zasięg i że w twoich testach występuje więcej niż jeden warunek brzegowy (np. Stan normalny vs. stan awarii).
Michael
6
Nie zgadzam się. Testy niekoniecznie muszą być SUCHE, ważniejsze jest, aby były DAMP (opisowe i znaczące zwroty) niż SUCHE. (Zasadniczo przynajmniej. W tym konkretnym przypadku wyciągnięcie powtórnej inicjalizacji do pomocnika zdecydowanie ma sens.)
Jörg W Mittag
2
Nigdy wcześniej nie słyszałem DAMP, ale podoba mi się ten opis.
Joachim Sauer,
@ Jörg W Mittag: Nadal możesz być SUCHY i WILGOTNY dzięki testom. Zazwyczaj przekierowuję różne części ARRANGE-ACT-ASSERT (lub GIVEN-WHEN-THEN) do metod pomocniczych w urządzeniu testowym, jeśli wiem, że jakaś część testu się powtarza. Zwykle mają nazwy DAMP, takie jak, givenThereAreProductsSet(amount)a nawet tak proste jak actWith(param). givenThereAre(2).products()Raz udało mi się to zrobić płynnie (np. ), Ale szybko przestałem, ponieważ czułem się jak przesada.
Spoike,
11

Jak już powiedział Oded, kod testowy nadal musi zostać utrzymany. Dodam, że powtarzanie w kodzie testowym utrudnia opiekunom zrozumienie struktury testów i dodawanie nowych testów.

W dwóch opublikowanych funkcjach następujące wiersze są absolutnie identyczne, z wyjątkiem jednej różnicy spacji na początku forpętli:

        int[] a = new int[1000];
        Random rand = new Random(DateTime.Now.Millisecond);
        for (int i = 0; i < a.Length; i++)
        {
            a[i] = rand.Next(Int16.MaxValue);
        }
        int[] b = new int[1000];
        a.CopyTo(b, 0);
        List<int> temp = b.ToList();
        temp.Sort();
        b = temp.ToArray();

Byłby to idealny kandydat do przejścia do jakiejś funkcji pomocniczej, której nazwa wskazuje, że inicjuje dane.

Clare Macrae
źródło
4

Nie, nie jest ok. Zamiast tego należy użyć TestDataBuilder . Powinieneś także zadbać o czytelność swoich testów: a? 1000? b ? Jeśli jutro będzie trzeba pracować nad implementacją, którą testujesz, testy to świetny sposób na wejście w logikę: napisz swoje testy dla was, innych programistów, a nie dla kompilatora :)

Oto Twoja implementacja testów, „przerobiona”:

/**
* Data your tests will exercice on
*/
public class MyTestData(){
    final int [] values;
    public MyTestData(int sampleSize){
        values = new int[sampleSize];
        //Out of scope of your question : Random IS a depencency you should manage
        Random rand = new Random(DateTime.Now.Millisecond);
        for (int i = 0; i < a.Length; i++)
        {
            a[i] = rand.Next(Int16.MaxValue);
        }
    }
    public int [] values();
        return values;
    }

}

/**
* Data builder, with default value. 
*/
public class MyTestDataBuilder {
    //1000 is actually your sample size : emphasis on the variable name
    private int sampleSize = 1000; //default value of the sample zie
    public MyTestDataBuilder(){
        //nope
    }
    //this is method if you need to test with another sample size
    public MyTestDataBuilder withSampleSizeOf(int size){
        sampleSize=size;
    }

    //call to get an actual MyTestData instance
    public MyTestData build(){
        return new MyTestData(sampleSize);
    }
}

public class MergeSortTest { 

    /**
    * Helper method build your expected data
    */
    private int [] getExpectedData(int [] source){
        int[] expectedData =  Arrays.copyOf(source,source.length);
        Arrays.sort(expectedData);
        return expectedData;
    }
}

//revamped tests method Merge
    public void MergeSortAssertArrayIsSorted(){
        int [] actualData = new MyTestDataBuilder().build();
        int [] expected = getExpectedData(actualData);
        //Don't know what 0 is for. An option, that should have a explicit name for sure :)
        MergeSort merge = new MergeSort();
        merge.mergeSort(actualData,0,actualData.length-1); 
        CollectionAssert.AreEqual(actualData, expected);
    }

 //revamped tests method Insertion
 public void InsertionSortAssertArrayIsSorted()
    {
        int [] actualData = new MyTestDataBuilder().build();
        int [] expected = getExpectedData(actualData);
        InsertionSort merge = new InsertionSort();
        merge.insertionSort(actualData);
        CollectionAssert.AreEqual(actualData, expectedData); 
    }
//another Test, for which very small sample size matter
public void doNotCrashesWithEmptyArray()
    {
        int [] actualData = new MyTestDataBuilder().withSampleSizeOf(0).build();
        int [] expected = getExpectedData(actualData);
        //continue ...
    }
}
Olivier
źródło
2

Co więcej niż kod produkcyjny, kod testowy musi być zoptymalizowany pod kątem czytelności i łatwości konserwacji, ponieważ musi być utrzymywany wzdłuż testowanego kodu, a także czytany jako część dokumentacji. Zastanów się, w jaki sposób skopiowany kod może utrudnić utrzymanie kodu testowego i jak może to stać się zachętą do nie pisania testów na wszystko. Nie zapominaj również, że kiedy piszesz funkcję do OSUSZANIA swoich testów, powinna ona również podlegać testom.

wariat
źródło
2

Powielanie kodu do testów jest łatwą pułapką. Pewnie, że jest to wygodne, ale co się stanie, jeśli zaczniesz refaktoryzować kod implementacyjny, a wszystkie testy zaczną wymagać zmiany? Ryzykujesz tak samo, jak w przypadku duplikacji kodu implementacji, ponieważ najprawdopodobniej będziesz musiał zmienić kod testowy również w wielu miejscach. Wszystko to składa się na ogromną stratę czasu i rosnącą liczbę punktów awarii, z którymi trzeba sobie poradzić, co oznacza, że ​​koszty utrzymania oprogramowania stają się niepotrzebnie wysokie, a tym samym zmniejszają ogólną wartość biznesową oprogramowania pracować nad.

Weź również pod uwagę, że to, co jest łatwe do zrobienia w testach, stanie się łatwe do wykonania we wdrożeniu. Kiedy odczuwasz presję czasu i stresu, ludzie polegają na wyuczonych wzorcach zachowań i zazwyczaj starają się robić to, co w danym momencie wydaje się najłatwiejsze. Tak więc, jeśli okaże się, że wycinasz i wklejasz dużo kodu testowego, prawdopodobnie zrobisz to samo w kodzie implementacyjnym, i to jest nawyk, którego chcesz unikać na początku swojej kariery, aby zaoszczędzić ci dużo trudności później, gdy będziesz musiał zachować starszy kod, który napisałeś, i że Twojej firmy niekoniecznie stać na przepisanie.

Jak powiedzieli inni, stosujesz zasadę SUCHA i szukasz okazji do refaktoryzacji wszelkich prawdopodobnych duplikacji metod pomocniczych i klas pomocniczych, i tak, powinieneś nawet robić to w testach, aby zmaksymalizować ponowne użycie kodu i zapisać później będziesz mieć trudności z utrzymaniem. Być może nawet powoli opracowujesz testowy interfejs API, którego możesz używać w kółko, być może nawet w wielu projektach - z pewnością tak właśnie się stało w ciągu ostatnich kilku lat.

S.Robins
źródło