Como 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 definidos para um valor que depende de sua posição na coleção. A maneira natural de fazer isso parece ser com um loop. No entanto, Roy Osherove recomenda strongmente contra o uso da lógica em testes unitários ( Art of Unit Testing , 178). Ele diz:

A test that contains logic is usually testing more than one thing at a time, whicfh isn't recommended, because the test is less readable and more fragile. But test logic also adds complexity that may contain a hidden bug.

Tests should, as a general rule, be a series of method calls with no control flows, not even try-catch, and with assert calls.

No entanto, não consigo ver nada de errado com meu design (de que outra forma você gera uma lista de objetos de dados, alguns dos quais dependem de onde eles estão? - não é possível gerar e testá-los exatamente separadamente). Existe algo não-teste-friendly com o meu design? Ou estou sendo muito rigidamente dedicado ao ensino de Osherove? Ou existe alguma magia de teste de unidade secreta que eu não conheço que contorna esse problema? (Estou escrevendo em C # / VS2010 / NUnit, mas procurando respostas independentes de linguagem, se possível.)

    
por Kazark 07.05.2013 / 18:58
fonte

3 respostas

13

TL; DR:

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

A primeira coisa para testar é sobre o dogma ser inútil. Eu gosto de ler O Caminho do Testivus , que aponta alguns problemas com o dogma de uma forma leve.

Write the test that needs to be written.

Se o teste precisar ser escrito de alguma forma, escreva dessa maneira. Tentando forçar o teste em algum layout de teste idealizado ou não tê-lo em tudo não é uma coisa boa. Fazer um teste hoje que testa é melhor do que ter um teste "perfeito" algum dia depois.

Também vou apontar para o teste feio:

When the code is ugly, the tests may be ugly.

You don’t like to write ugly tests, but ugly code needs testing the most.

Don’t let ugly code stop you from writing tests, but let ugly code stop you from writing more of it.

Estes podem ser considerados truísmos para aqueles que vêm acompanhando há muito tempo ... e eles apenas se tornam enraizados na maneira de pensar e escrever testes. Para pessoas que não foram e estão tentando chegar a esse ponto, os lembretes podem ser úteis (eu até acho que relê-los me ajuda a evitar ficar preso a algum dogma).

Considere que ao escrever um teste feio, se o código for 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 corretamente exercido ao escrever um teste simples, você pode considerar a possibilidade de 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 os "objetos de deus" são ruins, os "testes de unidade divinos" também são ruins e devem ser indicações para voltar e olhar o código novamente.

Você deve ser capaz de 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 questões maiores ("Eu tenho este objeto, enviado para o xml, enviado para o serviço da web, através das regras, de volta e desmarcado") é um excelente teste - mas certamente não é É um teste de unidade (e se enquadra no reino de testes de integração - mesmo que tenha burlado serviços que ele chama e personalizados em bancos de dados de memória para fazer o teste). Pode ainda usar o framework XUnit para testes, mas o framework de testes não faz dele um teste unitário.

    
por 07.05.2013 / 20:36
fonte
6

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

Eu disse na pergunta original

However, I can't see anything wrong with my design (how else do you generate a list of data objects, some of whose values are depended on where in the sequence they are?—can't exactly generate and test them separately)

Aqui é onde eu errei. Depois de fazer a programação funcional no último ano, percebo agora que precisava apenas de uma operação de coleta com um acumulador. Então eu poderia escrever minha função como uma função pura que operava 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 na maior parte do tempo. Você pode escrever suas funções para operar em coisas únicas e só aplicá-las a coleções de coisas no último momento. Mas se eles são puros você pode testá-los sem referência a coleções.

Para uma lógica mais complexa, apóie-se em testes baseados em propriedade . 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 caso que a pequena quantidade de lógica vale a pena.

Acima de tudo, apóie-se sempre nos seus tipos . Obtenha os tipos mais strongs que puder e use-os em seu benefício. Isso reduzirá o número de testes que você deve escrever em primeiro lugar.

    
por 03.09.2015 / 19:59
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 tamanho fixo, escreva um teste de unidade para validar o tamanho. Se for de comprimento variável, escreva vários testes para comprimentos que caracterizarão seu comportamento (por exemplo, 0, 1, 3, 10). De qualquer maneira, não valide as propriedades nesses testes.
  2. Escreva um teste de unidade para validar cada uma das propriedades. Se a coleção for de comprimento fixo e curta, basta fazer uma declaração contra uma propriedade de cada um dos elementos para cada teste. Se for de tamanho fixo, mas longo, escolha uma amostra representativa, mas pequena, dos elementos para fazer valer contra uma propriedade cada. Se for de tamanho variável, gere uma coleção relativamente curta, mas representativa (ou seja, talvez três elementos) e defina uma propriedade de cada.

Fazer isso dessa maneira faz com que os testes sejam pequenos o bastante para não parecer doloroso deixar de fora os laços. Exemplo de c # / unit, dado o método sob 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 com o paradigma "flat unit test" (sem estruturas aninhadas / lógica), esses testes parecem bem limpos. Assim, a lógica é evitada nos testes, identificando-se o problema original como uma tentativa de testar muitas propriedades de uma só vez, ao invés de perder loops.

    
por 07.05.2013 / 21:21
fonte