È corretto ripetere il codice per i test unitari?


11

Ho scritto alcuni algoritmi di ordinamento per un compito di classe e ho anche scritto alcuni test per assicurarmi che gli algoritmi fossero implementati correttamente. I miei test sono lunghi solo 10 righe e ce ne sono 3 ma solo 1 riga cambia tra le 3, quindi c'è un sacco di codice ripetuto. È meglio convertire questo codice in un altro metodo che viene quindi chiamato da ciascun test? Non avrei quindi bisogno di scrivere un altro test per testare il refactoring? Alcune delle variabili possono anche essere spostate al livello di classe. Le classi e i metodi di prova dovrebbero seguire le stesse regole delle classi / metodi regolari?

Ecco un esempio:

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

Risposte:


21

Il codice di prova è ancora codice e deve anche essere mantenuto.

Se è necessario modificare la logica copiata, è necessario farlo in ogni luogo in cui è stata copiata, normalmente.

DRY si applica ancora.

Non avrei quindi bisogno di scrivere un altro test per testare il refactoring?

Vorresti? E come fai a sapere se i test che hai attualmente sono corretti?

Testare il refactoring eseguendo i test. Dovrebbero avere tutti gli stessi risultati.


Va bene. I test sono codici - si applicano ancora gli stessi principi per scrivere un buon codice! Metti alla prova il refactoring eseguendo i test, ma assicurati che ci sia una copertura adeguata e che stai colpendo più di una condizione al contorno nei tuoi test (ad esempio una condizione normale contro una condizione di fallimento).
Michael,

6
Non sono d'accordo. I test non devono necessariamente essere DRY, è più importante che siano DAMP (frasi descrittive e significative) che DRY. (In generale, almeno. In questo caso specifico, tuttavia, è sicuramente logico tirare l'inizializzazione ripetuta in un aiuto.)
Jörg W Mittag

2
Non ho mai sentito DAMP prima, ma mi piace quella descrizione.
Joachim Sauer,

@ Jörg W Mittag: con i test puoi comunque essere DRY e DAMP. Di solito refactoring le diverse parti del test ARRANGE-ACT-ASSERT (o GIVEN-WHEN-THEN) per aiutare i metodi nel dispositivo di test se so che alcune parti del test si ripetono. Di solito hanno nomi DAMP, come givenThereAreProductsSet(amount)e anche semplici come actWith(param). Sono riuscito a farlo con api saggio fluente (ad esempio givenThereAre(2).products()) una volta, ma mi sono fermato rapidamente perché mi sembrava un eccesso.
Spoike,

11

Come già detto da Oded, il codice di test deve ancora essere mantenuto. Aggiungo che la ripetizione nel codice di test rende più difficile per i manutentori comprendere la struttura dei test e aggiungere nuovi test.

Nelle due funzioni che hai pubblicato, le seguenti righe sono assolutamente identiche ad eccezione di una differenza di spazio all'inizio del forloop:

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

Questo sarebbe un candidato perfetto per passare a una sorta di funzione di supporto, il cui nome indica che sta inizializzando i dati.


4

No, non va bene. In alternativa, è necessario utilizzare TestDataBuilder . Dovresti anche occuparti della leggibilità dei tuoi test: a? 1000? b? Se domani devi lavorare sull'implementazione che stai testando, i test sono un ottimo modo per entrare nella logica: scrivi i tuoi test per i tuoi colleghi programmatori, non per il compilatore :)

Ecco l'implementazione dei test "rinnovata":

/**
* 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 ...
    }
}

2

Ancor più del codice di produzione, il codice di test deve essere ottimizzato per la leggibilità e la manutenibilità, poiché deve essere mantenuto lungo il codice in fase di test ed essere letto come parte della documentazione. Considera come il codice copiato può rendere più difficile la manutenzione del codice di test e come ciò potrebbe diventare un incentivo a non scrivere test per tutto. Inoltre, non dimenticare che quando scrivi una funzione per ASCIUGARE i tuoi test, anche questo dovrebbe essere soggetto a test.


2

La duplicazione del codice per i test è una trappola facile in cui cadere. Sicuramente è conveniente, ma cosa succede se si inizia a refactoring il codice di implementazione e tutti i test iniziano a cambiare? Corri gli stessi rischi che hai se hai duplicato il tuo codice di implementazione, in quanto molto probabilmente dovrai cambiare il tuo codice di test anche in molti posti. Tutto ciò si traduce in una grande quantità di tempo sprecato e in un numero crescente di punti di errore che devono essere affrontati, il che significa che il costo per mantenere il software diventa inutilmente elevato e quindi riduce il valore aziendale complessivo del software lavorare su.

Considera anche che ciò che è facile da fare nei test diventerà facile da fare nell'implementazione. Quando sei a corto di tempo e molto stressato, le persone tendono a fare affidamento su modelli di comportamento appresi e generalmente cercano di fare ciò che sembra più semplice in quel momento. Quindi, se ti accorgi di tagliare e incollare molto del tuo codice di prova, è probabile che tu faccia lo stesso nel tuo codice di implementazione, e questa è un'abitudine che vuoi evitare all'inizio della tua carriera, per risparmiare molto di difficoltà in seguito quando ti ritrovi a dover mantenere il codice più vecchio che hai scritto e che la tua azienda non può necessariamente permettersi di riscrivere.

Come altri hanno già detto, applichi il principio DRY e cerchi opportunità per refactificare eventuali duplicazioni probabili ai metodi di supporto e alle classi di supporto, e sì, dovresti anche farlo nei tuoi test al fine di massimizzare il riutilizzo del codice e salvare se stessi affrontando difficoltà con la manutenzione in seguito. Potresti persino trovarti lentamente a sviluppare un'API di test che puoi utilizzare più e più volte, possibilmente anche in più progetti, sicuramente è così che sono successe le cose per me negli ultimi anni.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.