Est-il correct de répéter le code pour les tests unitaires?

11

J'ai écrit quelques algorithmes de tri pour une affectation de classe et j'ai également écrit quelques tests pour m'assurer que les algorithmes ont été correctement implémentés. Mes tests ne font que 10 lignes et il y en a 3 mais seulement 1 ligne change entre les 3 donc il y a beaucoup de code répété. Est-il préférable de refactoriser ce code dans une autre méthode qui est ensuite appelée à partir de chaque test? Ne devrais-je pas alors écrire un autre test pour tester le refactoring? Certaines variables peuvent même être déplacées au niveau de la classe. Les classes et méthodes de test doivent-elles suivre les mêmes règles que les classes / méthodes normales?

Voici un exemple:

    [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
la source

Réponses:

21

Le code de test est toujours du code et doit également être maintenu.

Si vous devez modifier la logique copiée, vous devez le faire à chaque endroit où vous l'avez copié, normalement.

DRY s'applique toujours.

Ne devrais-je pas alors écrire un autre test pour tester le refactoring?

Voudriez-vous? Et comment savez-vous que les tests que vous avez actuellement sont corrects?

Vous testez le refactoring en exécutant les tests. Ils devraient tous avoir les mêmes résultats.

Oded
la source
Oui. Les tests sont du code - les mêmes principes pour écrire un bon code s'appliquent toujours! Testez le refactoring en exécutant les tests, mais assurez-vous qu'il existe une couverture adéquate et que vous rencontrez plus d'une condition aux limites dans vos tests (par exemple, une condition normale par rapport à une condition d'échec).
Michael
6
Je ne suis pas d'accord. Les tests ne doivent pas nécessairement être SEC, il est plus important qu'ils soient DAMP (Phrases descriptives et significatives) que SEC. (En général, au moins. Dans ce cas spécifique, cependant, retirer l'initialisation répétée dans une aide a vraiment du sens.)
Jörg W Mittag
2
Je n'ai jamais entendu DAMP auparavant, mais j'aime cette description.
Joachim Sauer
@ Jörg W Mittag: Vous pouvez toujours être SEC et HUMIDE avec des tests. Je refactorise habituellement les différentes parties ARRANGE-ACT-ASSERT (ou GIVEN-WHEN-THEN) du test pour aider les méthodes dans le montage de test si je sais qu'une partie du test se répète. Ils ont généralement des noms DAMP, tels que givenThereAreProductsSet(amount)et même aussi simples que actWith(param). J'ai réussi à le faire avec une bonne maîtrise de l'api (par exemple givenThereAre(2).products()) une fois, mais j'ai rapidement arrêté parce que c'était comme une surpuissance.
Spoike
11

Comme Oded l'a déjà dit, le code de test doit encore être maintenu. J'ajouterais que la répétition dans le code de test rend plus difficile pour les mainteneurs de comprendre la structure des tests et d'ajouter de nouveaux tests.

Dans les deux fonctions que vous avez publiées, les lignes suivantes sont absolument identiques, à l'exception d'une différence d'espace au début de la forboucle:

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

Ce serait un candidat parfait pour passer à une sorte de fonction d'aide, dont le nom indique qu'il initialise des données.

Clare Macrae
la source
4

Non, ce n'est pas OK. Vous devez utiliser un TestDataBuilder à la place. Vous devez également faire attention à la lisibilité de vos tests: a? 1000? b? Si demain il faut travailler sur l'implémentation que vous testez, les tests sont un excellent moyen d'entrer dans la logique: écrivez vos tests pour vous autres programmeurs, pas pour le compilateur :)

Voici votre implémentation de tests, "revampée":

/**
* 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
la source
2

Encore plus que le code de production, le code de test doit être optimisé pour la lisibilité et la maintenabilité, car il doit être maintenu tout au long du code testé et également lu dans le cadre de la documentation. Considérez comment le code copié peut rendre la maintenance du code de test plus difficile et comment cela peut devenir une incitation à ne pas écrire de tests pour tout. N'oubliez pas non plus que lorsque vous écrivez une fonction pour sécher vos tests, elle doit également être soumise à des tests.

rbanffy
la source
2

La duplication de code pour les tests est un piège facile à tomber. Bien sûr, c'est pratique, mais que se passe-t-il si vous commencez à refactoriser votre code d'implémentation et que vos tests commencent tous à changer? Vous courez les mêmes risques que vous si vous avez dupliqué votre code d'implémentation, dans la mesure où vous devrez probablement changer votre code de test à de nombreux endroits également. Tout cela représente une perte de temps considérable et un nombre croissant de points de défaillance à traiter, ce qui signifie que le coût de maintenance de votre logiciel devient inutilement élevé et réduit donc la valeur commerciale globale du logiciel que vous travailler sur.

Considérez également que ce qui est facile à faire dans les tests deviendra facile à faire dans la mise en œuvre. Lorsque vous êtes pressé par le temps et que vous subissez beaucoup de stress, les gens ont tendance à s'appuyer sur des modèles de comportement appris et essaient généralement de faire ce qui semble le plus facile à l'époque. Donc, si vous trouvez que vous copiez et collez une grande partie de votre code de test, il est probable que vous fassiez de même dans votre code d'implémentation, et c'est une habitude que vous voulez éviter au début de votre carrière, pour vous faire économiser beaucoup de difficulté plus tard lorsque vous vous retrouvez devant conserver un code plus ancien que vous avez écrit et que votre entreprise ne peut pas nécessairement se permettre de réécrire.

Comme d'autres l'ont dit, vous appliquez le principal DRY et vous recherchez des opportunités de refactoriser toutes les duplications probables des méthodes et classes d'assistance, et oui, vous devriez même le faire dans vos tests afin de maximiser la réutilisation du code et d'enregistrer vous rencontrez des difficultés d'entretien plus tard. Vous pouvez même vous retrouver à développer lentement une API de test que vous pouvez utiliser encore et encore, peut-être même dans plusieurs projets - c'est certainement ainsi que les choses se sont passées pour moi au cours des dernières années.

S.Robins
la source