É correto repetir o código para testes de unidade?

11

Escrevi alguns algoritmos de classificação para uma atribuição de classe e também escrevi alguns testes para garantir que os algoritmos fossem implementados corretamente. Meus testes têm apenas 10 linhas e existem 3 deles, mas apenas 1 linha muda entre os 3, portanto há muito código repetido. É melhor refatorar esse código para outro método que é chamado a partir de cada teste? Eu não precisaria escrever outro teste para testar a refatoração? Algumas das variáveis ​​podem até ser movidas para o nível da classe. As classes e métodos de teste devem seguir as mesmas regras que as classes / métodos regulares?

Aqui está um exemplo:

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

Respostas:

21

O código de teste ainda é código e também precisa ser mantido.

Se você precisar alterar a lógica copiada, faça isso em todos os lugares em que a copiou, normalmente.

DRY ainda se aplica.

Eu não precisaria escrever outro teste para testar a refatoração?

Você iria? E como você sabe que os testes que você tem atualmente estão corretos?

Você testa a refatoração executando os testes. Todos devem ter os mesmos resultados.

Oded
fonte
Pode apostar. Os testes são código - todos os mesmos princípios para escrever um bom código ainda se aplicam! Teste a refatoração executando os testes, mas verifique se há cobertura adequada e se você está atingindo mais de uma condição de limite nos testes (por exemplo, uma condição normal vs. uma condição de falha).
Michael
6
Discordo. Os testes não precisam necessariamente ser SECOS, é mais importante que sejam DAMP (Frases Descritivas E Significativas) que DRY. (De um modo geral, pelo menos neste caso específico, porém, puxando para fora a inicialização repetida em um ajudante definitivamente faz sentido..)
Jörg W Mittag
2
Eu nunca ouvi DAMP antes, mas eu gosto dessa descrição.
Joachim Sauer
@ Jörg W Mittag: Você ainda pode estar seco e úmido com testes. Normalmente refatoro as diferentes partes de ARRANGE-ACT-ASSERT (ou GIVEN-WHEN-THEN) do teste para auxiliar os métodos no dispositivo de teste, se eu souber que alguma parte do teste se repete. Eles geralmente têm nomes DAMP, como givenThereAreProductsSet(amount)e até tão simples quanto actWith(param). Consegui fazer isso com uma API fluente (por exemplo givenThereAre(2).products()) uma vez, mas parei rapidamente porque parecia um exagero.
Spoike
11

Como Oded já disse, o código de teste ainda precisa ser mantido. Eu acrescentaria que a repetição no código de teste torna mais difícil para os mantenedores entender a estrutura dos testes e adicionar novos testes.

Nas duas funções que você postou, as seguintes linhas são absolutamente idênticas, exceto por uma diferença de espaço no início do 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();

Este seria um candidato perfeito para mudar para algum tipo de função auxiliar, cujo nome indica que está inicializando dados.

Clare Macrae
fonte
4

Não, não está ok. Você deve usar um TestDataBuilder . Você também deve cuidar da legibilidade de seus testes: a? 1000? b? Se amanhã for necessário trabalhar na implementação que você está testando, os testes são uma ótima maneira de entrar na lógica: escreva seus testes para seus colegas programadores, não para o compilador :)

Aqui está a sua implementação de testes, "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
fonte
2

Ainda mais do que o código de produção, o código de teste precisa ser otimizado para facilitar a leitura e a manutenção, pois precisa ser mantido ao longo do código que está sendo testado e também lido como parte da documentação. Considere como o código copiado pode dificultar a manutenção do código de teste e como isso pode se tornar um incentivo para não escrever testes para tudo. Além disso, não esqueça que quando você escreve uma função para secar seus testes, ela também deve estar sujeita a testes.

rbanffy
fonte
2

Duplicar código para testes é uma armadilha fácil de cair. Claro que é conveniente, mas o que acontece se você começar a refatorar o código de implementação e todos os testes começarem a precisar mudar? Você corre os mesmos riscos que você, se duplicou seu código de implementação, pois provavelmente também precisará alterar seu código de teste em muitos lugares. Isso tudo gera muito tempo perdido e um número crescente de pontos de falha que precisam ser resolvidos, o que significa que o custo para manter seu software se torna desnecessariamente alto e, portanto, reduz o valor comercial geral do software que você trabalho em.

Considere também que o que é fácil de fazer nos testes se tornará fácil na implementação. Quando você pressiona pelo tempo e sob muito estresse, as pessoas tendem a confiar nos padrões de comportamento aprendidos e geralmente tentam fazer o que parece mais fácil no momento. Portanto, se você achar que recorta e cola muito do seu código de teste, é provável que faça o mesmo no código de implementação, e esse é um hábito que você deseja evitar no início de sua carreira, para economizar muito de dificuldade mais tarde, quando você precisar manter o código mais antigo que você escreveu e que sua empresa não pode necessariamente se dar ao luxo de reescrever.

Como já foi dito, você aplica o princípio DRY e procura oportunidades para refatorar quaisquer duplicações prováveis ​​nos métodos e classes auxiliares; sim, você deve fazer isso nos testes para maximizar a reutilização do código e salvar você mesmo enfrentando dificuldades com a manutenção mais tarde. Você pode até se desenvolver lentamente uma API de teste que pode ser usada repetidamente, possivelmente até em vários projetos - certamente foi assim que as coisas aconteceram comigo nos últimos anos.

S.Robins
fonte