Como testar ao organizar os dados é muito complicado?

19

Estou escrevendo um analisador e, como parte disso, tenho uma Expanderclasse que "expande" uma única instrução complexa em várias instruções simples. Por exemplo, expandiria isso:

x = 2 + 3 * a

para dentro:

tmp1 = 3 * a
x = 2 + tmp1

Agora, estou pensando em como testar esta classe, especificamente em como organizar os testes. Eu poderia criar manualmente a árvore de sintaxe de entrada:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

Ou eu poderia escrever como uma string e analisá-la:

var input = new Parser().ParseStatement("x = 2 + 3 * a");

A segunda opção é muito mais simples, mais curta e legível. Mas também introduz uma denpendência Parser, o que significa que um erro Parserpode falhar neste teste. Então, o teste deixaria de ser um teste de unidade de Expander, e eu acho que tecnicamente se torna um teste de integração de Parsere Expander.

Minha pergunta é: é bom confiar principalmente (ou completamente) nesse tipo de teste de integração para testar essa Expanderclasse?

svick
fonte
3
O fato de um bug Parserpoder falhar em algum outro teste não é um problema se você normalmente comprometer apenas com zero falhas, pelo contrário, significa que você tem mais cobertura Parser. O que eu preferiria me preocupar é que um bug Parserpoderia fazer esse teste ser bem - sucedido quando deveria ter falhado . Afinal, os testes de unidade existem para encontrar bugs - um teste é interrompido quando não existe, mas deveria ter.
Jonas Kölker

Respostas:

27

Você se encontrará escrevendo muito mais testes, de comportamento muito mais complicado, interessante e útil, se puder fazê-lo com simplicidade. Então a opção que envolve

var input = new Parser().ParseStatement("x = 2 + 3 * a");

é bastante válido. Depende de outro componente. Mas tudo depende de dezenas de outros componentes. Se você zomba de algo dentro de uma polegada de sua vida, provavelmente está dependendo de muitos recursos de zombaria e acessórios de teste.

Às vezes, os desenvolvedores se concentram demais na pureza de seus testes de unidade ou no desenvolvimento de testes de unidade e testes de unidade apenas , sem nenhum módulo, integração, estresse ou outros tipos de testes. Todos esses formulários são válidos e úteis, e são de inteira responsabilidade dos desenvolvedores - não apenas do Q / A ou do pessoal de operações mais adiante.

Uma abordagem que usei é começar com essas execuções de nível mais alto e depois usar os dados produzidos a partir deles para construir a expressão do teste em forma longa e com o menor denominador comum. Por exemplo, quando você despeja a estrutura de dados do inputproduzido acima, pode facilmente construir o:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

tipo de teste que é testado no nível mais baixo. Dessa forma, você obtém uma mistura interessante: um punhado dos testes primitivos mais básicos (testes de unidade puros), mas não passou uma semana escrevendo testes nesse nível primitivo. Isso fornece o tempo necessário para escrever muitos outros testes atômicos um pouco menos atômicos Parser. Resultado final: mais testes, mais cobertura, mais detalhes e outros casos interessantes, melhor código e maior garantia de qualidade.

Jonathan Eunice
fonte
2
Isso é sensato - especialmente com relação ao fato de que tudo depende de muitos outros. Um bom teste de unidade deve testar o mínimo possível. Tudo o que estiver dentro dessa quantidade mínima possível deve ser testado por um teste de unidade anterior. Se você testou completamente o Parser, pode presumir que pode usá-lo com segurança para testar ParseStatement
Jon Story
6
A principal preocupação com a pureza (eu acho) é evitar escrever dependências circulares em seus testes de unidade. Se os testes do analisador ou do analisador usam o expansor, e esse teste depende do funcionamento do analisador, você tem um risco difícil de gerenciar de que tudo o que está testando é que o analisador e o expansor são consistentes , enquanto o que você queria testar se o expansor realmente faz o que deveria . Mas, desde que não haja dependência no sentido inverso, o uso do analisador neste teste de unidade não é diferente de usar uma biblioteca padrão em um teste de unidade.
Steve Jessop
@SteveJessop Good point. É importante usar componentes independentes .
Jonathan Eunice
3
Algo que fiz nos casos em que o analisador em si é uma operação cara (por exemplo, ler dados dos arquivos do Excel via interoperabilidade) é escrever métodos de geração de teste que executam o analisador e o código de saída no console para recriar a estrutura de dados que o analisador retorna. . Em seguida, copio a saída do gerador para testes de unidade mais convencionais. Isso permite reduzir a dependência cruzada, na medida em que o analisador só precisa estar funcionando corretamente quando os testes foram criados, nem sempre que são executados. (Sem perder alguns segundos / teste para criar / destruir processos Excel foi um bônus agradável.)
Dan Neely
+1 para a abordagem de @ DanNeely. Usamos algo semelhante para armazenar várias versões serializadas do nosso modelo de dados como dados de teste, para garantir que o novo código ainda funcione com dados mais antigos.
Chris Hayes
6

Claro que está tudo bem!

Você sempre precisa de testes funcionais / de integração que exercitem o caminho completo do código. E o caminho completo do código, neste caso, significa incluir a avaliação do código gerado. Ou seja, você testa que a análise x = 2 + 3 * aproduz código que se executado com a = 5será definido xcomo 17e se executado com a = -2será definido xcomo -4.

Abaixo disso, você deve fazer testes de unidade para bits menores , desde que isso realmente ajude a depurar o código . Os testes mais refinados que você terá, maior a probabilidade de que qualquer alteração no código também precise alterar o teste, porque a interface interna é alterada. Esse teste tem pouco valor a longo prazo e adiciona trabalho de manutenção. Portanto, há um ponto de retorno decrescente e você deve parar antes dele.

Jan Hudec
fonte
4

Os testes de unidade permitem fixar itens específicos pontuais que quebram e onde no código eles quebraram. Portanto, eles são bons para testes de granulação muito fina. Bons testes de unidade ajudarão a diminuir o tempo de depuração.

No entanto, pela minha experiência, os testes de unidade raramente são bons o suficiente para realmente verificar a operação correta. Portanto, os testes de integração também são úteis para verificar uma cadeia ou sequência de operações. Os testes de integração fazem você parte do caminho através de testes funcionais. Como você apontou, devido à complexidade dos testes de integração, é mais difícil encontrar o local específico no código em que o teste é interrompido. Ele também tem um pouco mais de fragilidade, pois falhas em qualquer lugar da cadeia causam falhas no teste. Você ainda terá essa cadeia no código de produção, portanto, testar a cadeia real ainda é útil.

Idealmente, você teria os dois, mas de qualquer forma, geralmente ter um teste automatizado é melhor do que não ter nenhum teste.

Peter Smith
fonte
0

Faça muitos testes no analisador e, à medida que o analisador passa nos testes, salve essas saídas em um arquivo para zombar do analisador e testar o outro componente.

Tulains Córdova
fonte