Como o método de teste de unidade que retorna uma coleção, evitando a lógica no teste

14

Estou testando um método que é gerar uma coleção de objetos de dados. Quero verificar se as propriedades dos objetos estão sendo definidas corretamente. Algumas das propriedades serão definidas para a mesma coisa; outros serão configurados para um valor que depende da sua posição na coleção. A maneira natural de fazer isso parece ser com um loop. No entanto, Roy Osherove recomenda fortemente o uso da lógica em testes de unidade ( Art of Unit Testing , 178). Ele diz:

Um teste que contém lógica geralmente está testando mais de uma coisa por vez, o que não é recomendado, porque o teste é menos legível e mais frágil. Mas a lógica de teste também adiciona complexidade que pode conter um bug oculto.

Os testes devem, como regra geral, ser uma série de chamadas de método sem fluxos de controle, nem mesmo try-catche com chamadas de confirmação.

No entanto, não vejo nada de errado com meu design (de que outra forma você gera uma lista de objetos de dados, alguns dos quais dependem de onde estão na sequência em que estão? - não podem exatamente gerá-los e testá-los separadamente). Existe algo que não seja amigável ao teste com meu design? Ou estou sendo muito rigidamente dedicado aos ensinamentos de Osherove? Ou há alguma mágica secreta de teste de unidade que eu não conheço que contorna esse problema? (Estou escrevendo em C # / VS2010 / NUnit, mas procurando respostas independentes do idioma, se possível.)

Kazark
fonte
4
Eu recomendo não fazer loop. Se o seu teste é que a terceira coisa tem a barra definida como Frob, escreva um teste para verificar especificamente se a barra da terceira coisa é Frob. Esse é um teste por si só, vá direto para ele, sem loop. Se o seu teste é obter uma coleção de 5 itens, também é um teste. Isso não quer dizer que você nunca tenha um loop (explícito ou não), apenas que muitas vezes não precisa. Além disso, trate o livro de Osherove como mais diretrizes do que regras reais.
Anthony Pegram
1
Os conjuntos @AnthonyPegram não são ordenados - às vezes, o Frob pode ser o 3º, às vezes o 2º. Você não pode confiar nisso, tornando necessário um loop (ou recurso de linguagem como o do Python in), se o teste for "Frob foi adicionado com sucesso a uma coleção existente".
Izkata
1
@ Izbata, sua pergunta menciona especificamente que a ordem é importante. Suas palavras: "outros serão definidos para um valor que depende da sua posição na coleção". Existem muitos tipos de coleção em C # (o idioma que ele faz referência) que são ordenados por inserção. Nesse caso, você também pode confiar no pedido com listas em Python, uma linguagem que você menciona.
Anthony Pegram
Além disso, digamos que você esteja testando um método Reset em uma coleção. Você precisa percorrer a coleção e verificar cada item. Dependendo do tamanho da coleção, não testá-lo em um loop é ridículo. Ou digamos que estou testando algo que deve incrementar cada item de uma coleção. Você pode definir todos os itens com o mesmo valor, chamar seu incremento e depois verificar. Esse teste é péssimo. Você deve definir vários deles com valores diferentes, chamar incremento e verificar se todos os valores diferentes foram incrementados corretamente. Verificar apenas um item aleatório na coleção está deixando muito ao acaso.
Iheanyi
Não vou responder dessa maneira, porque receberei um bilhão de votos negativos, mas muitas vezes apenas toString()coleciono e comparo com o que deveria ser. Simples e funciona.
user949300

Respostas:

16

TL; DR:

  • Escreva o teste
  • Se o teste fizer muito, o código também pode fazer muito.
  • Pode não ser um teste de unidade (mas não é um teste ruim).

A primeira coisa a ser testada é sobre o dogma ser inútil. Gosto de ler The Way of Testivus, que aponta alguns problemas com o dogma de uma maneira alegre.

Escreva o teste que precisa ser escrito.

Se o teste precisar ser escrito de alguma forma, escreva dessa maneira. Tentar forçar o teste a um layout de teste idealizado ou não tê-lo, não é uma coisa boa. Ter um teste hoje que testa é melhor do que ter um teste "perfeito" mais tarde.

Também vou apontar para o pouco no teste feio:

Quando o código é feio, os testes podem ser feios.

Você não gosta de escrever testes feios, mas códigos feios precisam mais de testes.

Não deixe que o código feio impeça você de escrever testes, mas deixe que o código feio impeça você de escrever mais.

Estes podem ser considerados truques para aqueles que têm seguido por um longo tempo ... e eles se tornam arraigados na maneira de pensar e escrever testes. Para pessoas que não estiveram e estão tentando chegar a esse ponto, os lembretes podem ser úteis (acho até que relê-los me ajuda a evitar ficar preso em algum dogma).


Considere que, ao escrever um teste feio, se o código puder ser uma indicação de que o código está tentando fazer muito também. Se o código que você está testando for muito complexo para ser exercitado adequadamente escrevendo um teste simples, considere dividir o código em partes menores que podem ser testadas com os testes mais simples. Não se deve escrever um teste de unidade que faça tudo (pode não ser um teste de unidade ). Assim como 'objetos de Deus' são ruins, 'testes de unidade de Deus' também são ruins e devem ser indicações para voltar e olhar o código novamente.

Você deve poder exercitar todo o código com uma cobertura razoável por meio de testes simples. Testes que fazem mais testes de ponta a ponta que lidam com perguntas maiores ("Eu tenho esse objeto, empacotado em xml, enviado ao serviço da web, através das regras, de volta e não empacotado") é um excelente teste - mas certamente não é é um teste de unidade (e cai no domínio de teste de integração - mesmo que tenha zombado dos serviços que chama e customizado nos bancos de dados de memória para fazer o teste). Ele ainda pode usar a estrutura do XUnit para teste, mas a estrutura de teste não o torna um teste de unidade.


fonte
7

Estou adicionando uma nova resposta porque minha perspectiva é diferente de quando escrevi a pergunta e a resposta originais; não faz sentido combiná-los juntos em um.

Eu disse na pergunta original

No entanto, não vejo nada de errado com meu design (de que outra forma você gera uma lista de objetos de dados, cujos valores dependem de onde estão na sequência em que estão? - não podem exatamente gerá-los e testá-los separadamente)

Foi aqui que eu errei. Depois de fazer a programação funcional do ano passado, agora percebo que eu só precisava de uma operação de coleta com um acumulador. Então eu poderia escrever minha função como uma função pura que funcionava em uma coisa e usar alguma função de biblioteca padrão para aplicá-la à coleção.

Portanto, minha nova resposta é: use técnicas de programação funcional e você evitará esse problema inteiramente a maior parte do tempo. Você pode escrever suas funções para operar com itens únicos e aplicá-las apenas a coleções de itens no último momento. Mas se forem puros, você poderá testá-los sem fazer referência a coleções.

Para uma lógica mais complexa, use testes baseados em propriedades . Quando eles têm lógica, ela deve ser menor e inversa à lógica do código em teste, e cada teste verifica muito mais do que um teste de unidade baseado em casos que a pequena quantidade de lógica vale a pena.

Acima de tudo, incline-se sempre nos seus tipos . Obtenha os tipos mais fortes que puder e use-os para sua vantagem. Isso reduzirá o número de testes que você precisa escrever em primeiro lugar.

Kazark
fonte
4

Não tente testar muitas coisas ao mesmo tempo. Cada uma das propriedades de cada objeto de dados na coleção é demais para um teste. Em vez disso, recomendo:

  1. Se a coleção for de comprimento fixo, escreva um teste de unidade para validar o comprimento. Se tiver comprimento variável, escreva vários testes para comprimentos que caracterizem seu comportamento (por exemplo, 0, 1, 3, 10). De qualquer forma, não valide propriedades nesses testes.
  2. Escreva um teste de unidade para validar cada uma das propriedades. Se a coleção for de tamanho fixo e curto, apenas afirme contra uma propriedade de cada um dos elementos para cada teste. Se o seu comprimento for fixo, mas longo, escolha uma amostra representativa, mas pequena, dos elementos a serem declarados em uma propriedade cada. Se for de tamanho variável, gere uma coleção relativamente curta, mas representativa (ou seja, talvez três elementos) e faça uma declaração contra uma propriedade de cada.

Fazer dessa maneira torna os testes pequenos o suficiente para que deixar de fora os loops não pareça doloroso. Exemplo de C # / Unidade, com o método em teste ICollection<Foo> generateFoos(uint numberOfFoos):

[Test]
void generate_zero_foos_returns_empty_list() { ... }
void generate_one_foo_returns_list_of_one_foo() { ... }
void generate_three_foos_returns_list_of_three_foos() { ... }
void generated_foos_have_sequential_ID()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("ID1", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID2", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID3", foos.Current.id);
}
void generated_foos_have_bar()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
}

Se você está acostumado ao paradigma "teste de unidade plana" (sem estruturas / lógica aninhadas), esses testes parecem bastante limpos. Assim, a lógica é evitada nos testes, identificando o problema original como tentando testar muitas propriedades ao mesmo tempo, em vez de não ter loops.

Kazark
fonte
1
Osherove teria sua cabeça em uma bandeja por ter 3 afirmações. ;) O primeiro a falhar significa que você nunca valida o resto. Observe também que você realmente não evitou o loop. Você apenas o expandiu explicitamente para a forma executada. Não é uma crítica difícil, mas apenas uma sugestão para que você pratique mais o isolamento de seus casos de teste na quantidade mínima possível, para fornecer um feedback mais específico quando algo falhar, enquanto continua validando outros casos que ainda podem passar (ou falhar, com seus próprios feedbacks específicos).
Anthony Pegram
3
@AnthonyPegram Conheço o paradigma de uma afirmação por teste. Prefiro o mantra "testar uma coisa" (como defendido por Bob Martin, contra uma afirmação por teste, no Código Limpo ). Nota lateral: estruturas de teste de unidade que têm "expectativa" versus "afirmação" são boas (Google Tests). Quanto ao resto, por que você não divide suas sugestões em uma resposta completa, com exemplos? Eu acho que poderia me beneficiar.
Kazark 8/13