Como você mantém eficientemente seus testes funcionando enquanto reprojeta?

14

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?

Alex Feinman
fonte
Como isso é significativamente diferente de programmers.stackexchange.com/questions/5898/… ?
precisa saber é o seguinte
4
Essa pergunta equivocada sobre refatoração - testes de unidade devem ser invariantes na refatoração.
Alex Feinman

Respostas:

9

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.

Conta
fonte
Eu acho que você quis escrever "fora do módulo", não "fora do aplicativo".
SamB 21/09
SamB, isso depende. Se a interface for interna a alguns lugares de um aplicativo, mas não pública, eu consideraria o teste em um nível mais alto se achasse que a interface provavelmente é volátil.
Bill
Eu achei essa abordagem muito compatível com o TDD. Gosto de começar nas camadas superiores do aplicativo mais perto do usuário final, para que eu possa projetar as camadas inferiores sabendo como as camadas superiores precisam usar as camadas inferiores. A criação essencial de cima para baixo permite projetar com mais precisão a interface entre uma camada e outra.
22418 Greg Greghardt
4

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:

  • Excluídas as classes que eu estava substituindo.
  • Excluídos os conjuntos de testes para essas classes.
  • Deixou o conjunto de testes específico para TIdSipTcpTransport (e ainda havia o conjunto de testes comum a todos os TIdSipTransports).
  • Execute os testes TIdSipTransport / TIdSipTcpTransport, para garantir que todos falhem.
  • Comentou todos, exceto um teste TIdSipTransport / TIdSipTcpTransport.
  • Se eu precisasse adicionar uma classe, adicionaria testes de gravação para criar funcionalidade suficiente para que o único teste não comentado passasse.
  • Espuma, enxágüe, repita.

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.

Frank Shearar
fonte
Fiz isso há alguns meses e funcionou muito bem para mim. No entanto, não pude aplicar esse método de maneira absoluta ao emparelhar com um colega na reformulação inicial do nosso módulo de modelo de domínio (que, por sua vez, acionou a reformulação de todos os outros módulos do projeto).
Marco Ciambrone
3

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:

  • Gere o HTML como um teste de fumaça para garantir que ele realmente seja executado
  • Use um sistema de modelos, para poder testar o processador e os dados enviados ao modelo, sem testar o próprio modelo exato.

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.

Winston Ewert
fonte
Também pode ajudar a usar HTML estrutural.
SamB 21/09
@SamB certamente que iria ajudar, mas eu não acho que vai resolver o problema completamente
Winston Ewert
claro que não, nada pode :-)
Samb
-1

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).

Lewis Pringle
fonte
Nunca acrescente sufixos que não tenham significado ou que sejam autodepreciativos para nomes. Sempre se esforce para dar nomes significativos.
Basilevs
Você perdeu completamente o objetivo. Primeiro - não são sufixos que "não têm significado". Eles carregam o significado de que a API está fazendo a transição de uma mais antiga para uma mais nova. De fato, esse é o ponto principal da PERGUNTA à qual eu estava respondendo e o ponto inteiro da resposta. Os nomes CLEARLY comunicam qual é a API OLD, qual é a API NEW e qual é o nome de destino da API quando a transição estiver concluída. AND - os sufixos _OLD / _NEW são temporários - SOMENTE durante a transição de alteração da API.
21418 Lewis Pringle
Boa sorte com a versão NEW_NEW_3 da API três anos depois.
Basilevs