Ist es in Ordnung, Code für Unit-Tests zu wiederholen?

11

Ich habe einige Sortieralgorithmen für eine Klassenzuweisung geschrieben und einige Tests geschrieben, um sicherzustellen, dass die Algorithmen korrekt implementiert wurden. Meine Tests sind nur 10 Zeilen lang und es gibt 3 davon, aber nur 1 Zeile wechselt zwischen den 3, so dass es viel wiederholten Code gibt. Ist es besser, diesen Code in eine andere Methode umzugestalten, die dann von jedem Test aufgerufen wird? Müsste ich dann nicht einen weiteren Test schreiben, um das Refactoring zu testen? Einige der Variablen können sogar auf Klassenebene verschoben werden. Sollten Testklassen und -methoden denselben Regeln folgen wie reguläre Klassen / Methoden?

Hier ist ein Beispiel:

    [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
quelle

Antworten:

21

Testcode ist immer noch Code und muss ebenfalls gepflegt werden.

Wenn Sie die kopierte Logik ändern müssen, müssen Sie dies normalerweise an jedem Ort tun, an den Sie sie kopiert haben.

DRY gilt immer noch.

Müsste ich dann nicht einen weiteren Test schreiben, um das Refactoring zu testen?

Würdest du? Und woher wissen Sie, dass die Tests, die Sie derzeit haben, korrekt sind?

Sie testen das Refactoring, indem Sie die Tests ausführen. Sie sollten alle die gleichen Ergebnisse haben.

Oded
quelle
Direkt am. Tests sind Code - es gelten immer noch die gleichen Prinzipien für das Schreiben von gutem Code! Testen Sie das Refactoring, indem Sie die Tests ausführen. Stellen Sie jedoch sicher, dass eine ausreichende Abdeckung vorhanden ist und dass Sie in Ihren Tests mehr als eine Randbedingung erfüllen (z. B. eine normale Bedingung im Vergleich zu einer Fehlerbedingung).
Michael
6
Ich stimme dir nicht zu. Tests müssen nicht unbedingt trocken sein, es ist wichtiger, dass sie DAMP (Descriptive And Meaningful Phrases) sind als DRY. (Zumindest im Allgemeinen. In diesem speziellen Fall ist es jedoch definitiv sinnvoll, die wiederholte Initialisierung in einen Helfer zu ziehen.)
Jörg W Mittag
2
Ich habe DAMP noch nie gehört, aber ich mag diese Beschreibung.
Joachim Sauer
@ Jörg W Mittag: Mit Tests kann man immer noch trocken und feucht sein. Normalerweise überarbeite ich die verschiedenen ARRANGE-ACT-ASSERT- (oder GIVEN-WHEN-THEN-) Teile des Tests, um die Methoden in der Testvorrichtung zu unterstützen, wenn ich weiß, dass sich ein Teil des Tests wiederholt. Sie haben normalerweise DAMP-Namen, wie givenThereAreProductsSet(amount)und sogar so einfach wie actWith(param). Ich habe es einmal mit fließendem API geschafft (zB givenThereAre(2).products()), aber ich habe schnell aufgehört, weil es sich wie ein Overkill anfühlte.
Spoike
11

Wie Oded bereits sagte, muss der Testcode noch beibehalten werden. Ich möchte hinzufügen, dass die Wiederholung im Testcode es den Betreuern erschwert, die Struktur von Tests zu verstehen und neue Tests hinzuzufügen.

In den beiden von Ihnen geposteten Funktionen sind die folgenden Zeilen bis auf einen Leerzeichenunterschied am Anfang der forSchleife absolut identisch :

        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();

Dies wäre ein perfekter Kandidat für den Wechsel zu einer Art Hilfsfunktion, deren Name darauf hinweist, dass Daten initialisiert werden.

Clare Macrae
quelle
4

Nein, es ist nicht in Ordnung. Sie sollten stattdessen einen TestDataBuilder verwenden. Sie sollten auch auf die Lesbarkeit Ihrer Tests achten: a? 1000? b? Wenn Sie morgen an der Implementierung arbeiten müssen, die Sie testen, sind Tests eine gute Möglichkeit, in die Logik einzutreten: Schreiben Sie Ihre Tests für Ihre Programmierkollegen, nicht für den Compiler :)

Hier ist Ihre Testimplementierung, "überarbeitet":

/**
* 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
quelle
2

Noch mehr als Produktionscode muss der Testcode hinsichtlich Lesbarkeit und Wartbarkeit optimiert werden, da er entlang des zu testenden Codes gepflegt und auch als Teil der Dokumentation gelesen werden muss. Überlegen Sie, wie kopierter Code die Wartung von Testcodes erschweren kann und wie dies zu einem Anreiz werden kann, nicht für alles Tests zu schreiben. Vergessen Sie auch nicht, dass beim Schreiben einer Funktion zum TROCKNEN Ihrer Tests auch Tests durchgeführt werden sollten.

rbanffy
quelle
2

Das Duplizieren von Code für Tests ist eine einfache Falle. Sicher, es ist praktisch, aber was passiert, wenn Sie anfangen, Ihren Implementierungscode zu überarbeiten, und Ihre Tests sich alle ändern müssen? Sie gehen die gleichen Risiken ein, die Sie eingehen, wenn Sie Ihren Implementierungscode dupliziert haben, da Sie Ihren Testcode höchstwahrscheinlich auch an vielen Stellen ändern müssen. Dies alles führt zu einer erheblichen Zeitverschwendung und einer zunehmenden Anzahl von Fehlerpunkten, die behoben werden müssen. Dies bedeutet, dass die Kosten für die Wartung Ihrer Software unnötig hoch werden und somit den Gesamtgeschäftswert der von Ihnen verwendeten Software verringern arbeiten an.

Bedenken Sie auch, dass das, was in Tests einfach zu tun ist, in der Implementierung einfach zu tun sein wird. Wenn Sie unter Zeitdruck stehen und unter viel Stress stehen, verlassen sich die Menschen in der Regel auf erlernte Verhaltensmuster und versuchen im Allgemeinen, das zu tun, was zu diesem Zeitpunkt am einfachsten erscheint. Wenn Sie also feststellen, dass Sie einen Großteil Ihres Testcodes ausschneiden und einfügen, werden Sie wahrscheinlich dasselbe in Ihrem Implementierungscode tun. Dies ist eine Gewohnheit, die Sie zu Beginn Ihrer Karriere vermeiden möchten, um viel zu sparen Dies ist später schwierig, wenn Sie feststellen müssen, dass Sie älteren Code, den Sie geschrieben haben, beibehalten müssen und Ihr Unternehmen es sich nicht unbedingt leisten kann, ihn neu zu schreiben.

Wie andere bereits gesagt haben, wenden Sie das DRY-Prinzip an und suchen nach Möglichkeiten, mögliche Duplikate für Hilfsmethoden und Hilfsklassen umzugestalten. Ja, Sie sollten dies sogar in Ihren Tests tun, um die Wiederverwendung von Code zu maximieren und zu speichern Sie haben später Schwierigkeiten mit der Wartung. Möglicherweise entwickeln Sie sogar langsam eine Test-API, die Sie immer wieder verwenden können, möglicherweise sogar in mehreren Projekten - genau so ist es mir in den letzten Jahren ergangen.

S.Robins
quelle