Uma base de código bem testada tem vários benefícios, mas testar certos aspectos do sistema resulta em uma base de código resistente a alguns tipos de alterações.
Um exemplo é testar a saída específica - por exemplo, texto ou HTML. Os testes geralmente são escritos (ingenuamente?) Para esperar um bloco de texto específico como saída para alguns parâmetros de entrada ou para procurar seções específicas em um bloco.
Alterar o comportamento do código, para atender a novos requisitos ou porque os testes de usabilidade resultaram em alterações na interface, também requer alterações nos testes - talvez até testes que não sejam especificamente testes de unidade para o código que está sendo alterado.
Como você gerencia o trabalho de encontrar e reescrever esses testes? E se você não pode simplesmente "executá-los todos e deixar a estrutura os separar"?
Que outros tipos de código em teste resultam em testes habitualmente frágeis?
fonte
Respostas:
Eu sei que o pessoal do TDD vai odiar essa resposta, mas uma grande parte dela é para escolher com cuidado onde testar alguma coisa.
Se eu ficar louco demais com os testes de unidade nos níveis mais baixos, nenhuma alteração significativa poderá ser feita sem alterar os testes de unidade. Se a interface nunca for exposta e não pretender ser reutilizada fora do aplicativo, isso será uma sobrecarga desnecessária para o que poderia ter sido uma mudança rápida caso contrário.
Por outro lado, se o que você está tentando mudar for exposto ou reutilizado em todos os testes que você precisará alterar, é evidência de algo que você pode estar quebrando em outro lugar.
Em alguns projetos, isso pode significar projetar seus testes do nível de aceitação para baixo, e não dos testes de unidade. e ter menos testes de unidade e mais testes de estilo de integração.
Isso não significa que você ainda não pode identificar um único recurso e código até que esse recurso atenda aos seus critérios de aceitação. Simplesmente significa que, em alguns casos, você não acaba medindo os critérios de aceitação com testes de unidade.
fonte
Acabei de concluir uma grande revisão da minha pilha SIP, reescrevendo todo o transporte TCP. (Era quase um refator, em grande escala, em relação à maioria das refatorações.)
Em resumo, há uma subclasse TIdSipTcpTransport. Todos os TIdSipTransports compartilham um conjunto de testes comum. Dentro do TIdSipTcpTransport havia várias classes - um mapa contendo pares de conexão / mensagem de inicialização, clientes TCP encadeados, um servidor TCP encadeado e assim por diante.
Aqui está o que eu fiz:
Assim, eu sabia o que ainda precisava fazer, na forma dos testes comentados (*), e sabia que o novo código estava funcionando como esperado, graças aos novos testes que escrevi.
(*) Realmente, você não precisa comentar. Apenas não os execute; 100 testes com falha não são muito encorajadores. Além disso, em minha configuração específica, compilar menos testes significa um loop de teste-gravação-refator mais rápido.
fonte
Quando os testes são frágeis, acho que geralmente é porque estou testando a coisa errada. Tomemos, por exemplo, saída HTML. Se você verificar a saída HTML real, seu teste será frágil. Mas você não está interessado no resultado real, está interessado em saber se ele transmite as informações que deveria. Infelizmente, isso exige afirmações sobre o conteúdo do cérebro do usuário e, portanto, não pode ser feito automaticamente.
Você pode:
O mesmo tipo de coisa acontece com o SQL. Se você afirmar o SQL real, suas classes tentarão criar problemas. Você realmente deseja afirmar os resultados. Portanto, eu uso um banco de dados de memória SQLITE durante meus testes de unidade para garantir que meu SQL realmente faça o que deveria.
fonte
Primeiro, crie uma NEW API, que faça o que você deseja que seja o seu comportamento da NEW API. Se essa nova API tiver o mesmo nome que uma API ANTIGA, anexarei o nome _NEW ao novo nome da API.
int DoSomethingInterestingAPI ();
torna-se:
int DoSomethingInterestingAPI_NEW (int takes_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (Whatever_default_mimics_the_old_API); OK - nesse estágio - todos os seus testes de regressão ainda passam - usando o nome DoSomethingInterestingAPI ().
NEXT, consulte seu código e altere todas as chamadas para DoSomethingInterestingAPI () para a variante apropriada de DoSomethingInterestingAPI_NEW (). Isso inclui atualizar / reescrever quaisquer partes de seus testes de regressão que precisam ser alteradas para usar a nova API.
NEXT, marque DoSomethingInterestingAPI_OLD () como [[obsoleto ()]]. Mantenha a API reprovada pelo tempo que desejar (até atualizar com segurança todo o código que possa depender dela).
Com essa abordagem, quaisquer falhas em seus testes de regressão são simplesmente erros no teste de regressão ou identificam erros no seu código - exatamente como você deseja. Esse processo faseado de revisar uma API criando explicitamente as versões _NEW e _OLD da API permite que você coexista por um tempo bits do código novo e antigo.
Aqui está um bom exemplo (difícil) dessa abordagem na prática. Eu tinha a função BitSubstring () - onde havia usado a abordagem de ter o terceiro parâmetro como a CONTAGEM de bits na substring. Para ser consistente com outras APIs e padrões em C ++, eu queria mudar para o início / fim como argumentos para a função.
https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0
Criei uma função BitSubstring_NEW com a nova API e atualizei todo o meu código para usá-la (deixando NO MORE CHAMADAS para o BitSubString). Mas eu deixei a implementação por vários lançamentos (meses) - e o marquei como obsoleto - para que todos pudessem mudar para BitSubString_NEW (e naquele momento alterar o argumento de uma contagem para o estilo de início / fim).
ENTÃO - quando essa transição foi concluída, fiz outro commit excluindo BitSubString () e renomeando BitSubString_NEW-> BitSubString () (e preteri o nome BitSubString_NEW).
fonte