¿Está bien repetir el código para las pruebas unitarias?

11

Escribí algunos algoritmos de clasificación para una tarea de clase y también escribí algunas pruebas para asegurarme de que los algoritmos se implementaron correctamente. Mis pruebas son solo como 10 líneas de largo y hay 3 de ellas, pero solo 1 línea cambia entre las 3, por lo que hay mucho código repetido. ¿Es mejor refactorizar este código en otro método que luego se llama desde cada prueba? ¿No necesitaría escribir otra prueba para probar la refactorización? Algunas de las variables pueden incluso moverse al nivel de la clase. ¿Deben las clases y métodos de prueba seguir las mismas reglas que las clases / métodos normales?

Aquí hay un ejemplo:

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

Respuestas:

21

El código de prueba sigue siendo código y también debe mantenerse.

Si necesita cambiar la lógica copiada, debe hacerlo en todos los lugares donde la copió, normalmente.

SECO todavía se aplica.

¿No necesitaría escribir otra prueba para probar la refactorización?

¿Lo harías? ¿Y cómo sabe que las pruebas que tiene actualmente son correctas?

Usted prueba la refactorización ejecutando las pruebas. Todos deberían tener los mismos resultados.

Oded
fuente
Tocar el asunto exacto. Las pruebas son código: ¡todavía se aplican los mismos principios para escribir un buen código! Pruebe la refactorización ejecutando las pruebas, pero asegúrese de que haya una cobertura adecuada y que esté alcanzando más de una condición límite en sus pruebas (por ejemplo, una condición normal frente a una condición de falla).
Michael
66
Estoy en desacuerdo. Las pruebas no necesariamente tienen que estar SECAS, es más importante que sean DAMP (Frases descriptivas y significativas) que SECAS. (En general, al menos. Sin embargo, en este caso específico, sacar la inicialización repetida en un ayudante definitivamente tiene sentido.)
Jörg W Mittag
2
Nunca antes había escuchado DAMP, pero me gusta esa descripción.
Joachim Sauer
@ Jörg W Mittag: aún puede estar SECO y AMORTIGUADO con las pruebas. Por lo general, refactorizo ​​las diferentes partes ARRANGE-ACT-ASSERT (o GIVEN-WHEN-THEN) de la prueba para ayudar a los métodos en el dispositivo de prueba si sé que alguna parte de la prueba se repite. Por lo general, tienen nombres DAMP, como givenThereAreProductsSet(amount)e incluso tan simple como actWith(param). Me las arreglé para hacerlo con una aplicación fluida (p givenThereAre(2).products(). Ej. ) Una vez, pero me detuve rápidamente porque me pareció una exageración.
Spoike
11

Como ya dijo Oded, el código de prueba aún debe mantenerse. Agregaría que la repetición en el código de prueba hace que sea más difícil para los mantenedores comprender la estructura de las pruebas y agregar nuevas pruebas.

En las dos funciones que publicó, las siguientes líneas son absolutamente idénticas, excepto por una diferencia de espacio al comienzo del forciclo:

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

Este sería un candidato perfecto para pasar a algún tipo de función auxiliar, cuyo nombre indica que está inicializando datos.

Clara Macrae
fuente
4

No, no está bien. En su lugar, debe usar un TestDataBuilder . También debe tener cuidado con la legibilidad de sus pruebas: a? 1000? b? Si mañana uno tiene que trabajar en la implementación que está probando, las pruebas son una excelente manera de ingresar a la lógica: escriba sus pruebas para sus compañeros programadores, no para el compilador :)

Aquí está su implementación de pruebas, "renovada":

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

Incluso más que el código de producción, el código de prueba debe optimizarse para facilitar su lectura y mantenimiento, ya que debe mantenerse junto con el código que se está probando y también debe leerse como parte de la documentación. Considere cómo el código copiado puede dificultar el mantenimiento del código de prueba y cómo eso puede convertirse en un incentivo para no escribir pruebas para todo. Además, no olvide que cuando escribe una función para SECAR sus pruebas, también debe estar sujeta a pruebas.

rbanffy
fuente
2

Duplicar el código para las pruebas es una trampa fácil en la que caer. Claro que es conveniente, pero ¿qué sucede si comienzas a refactorizar tu código de implementación y todas tus pruebas comienzan a necesitar cambiar? Corre los mismos riesgos que corre si ha duplicado su código de implementación, ya que lo más probable es que también necesite cambiar su código de prueba en muchos lugares. Todo esto se suma a una gran cantidad de tiempo perdido y a un número cada vez mayor de puntos de falla que deben abordarse, lo que significa que el costo para mantener su software se vuelve innecesariamente alto y, por lo tanto, reduce el valor comercial general del software que usted trabajar en.

Considere también que lo que es fácil de hacer en las pruebas será más fácil de hacer en la implementación. Cuando tiene poco tiempo y mucho estrés, las personas tienden a confiar en los patrones de comportamiento aprendidos y generalmente intentan hacer lo que parece más fácil en ese momento. Entonces, si encuentra que corta y pega mucho código de prueba, es probable que haga lo mismo en su código de implementación, y este es un hábito que desea evitar al principio de su carrera, para ahorrarle mucho de dificultad más adelante cuando tenga que mantener el código anterior que ha escrito y que su empresa no pueda permitirse reescribir necesariamente.

Como han dicho otros, usted aplica el director DRY, y busca oportunidades para refactorizar cualquier duplicación probable a métodos auxiliares y clases auxiliares, y sí, incluso debería hacerlo en sus pruebas para maximizar la reutilización del código y guardar usted mismo enfrenta dificultades con el mantenimiento más adelante. Incluso puede encontrarse lentamente desarrollando una API de prueba que puede usar una y otra vez, posiblemente incluso en múltiples proyectos, ciertamente así es como me han sucedido las cosas en los últimos años.

S.Robins
fuente