Como evitar testes de unidade frágeis?

24

Escrevemos perto de 3.000 testes - os dados foram codificados, com pouca reutilização de código. Essa metodologia começou a nos morder na bunda. À medida que o sistema muda, nos vemos gastando mais tempo consertando testes quebrados. Temos testes unitários, de integração e funcionais.

O que estou procurando é uma maneira definitiva de escrever testes gerenciáveis ​​e de manutenção.

Frameworks

Chuck Conway
fonte
Isso é muito mais adequado para Programmers.StackExchange, IMO ...
iAbstract
BDD
Robbie Dee

Respostas:

21

Não pense neles como "testes de unidade quebrados", porque não são.

São especificações que o seu programa não oferece mais suporte.

Não pense nisso como "corrigindo os testes", mas como "definindo novos requisitos".

Os testes devem especificar seu aplicativo primeiro, e não o contrário.

Você não pode dizer que possui uma implementação funcional até saber que funciona. Você não pode dizer que funciona até testá-lo.

Algumas outras notas que podem guiá-lo:

  1. Os testes e as classes em teste devem ser breves e simples . Cada teste deve verificar apenas uma parte coesa da funcionalidade. Ou seja, ele não se importa com coisas que outros testes já verificam.
  2. Os testes e seus objetos devem ser fracamente acoplados, de forma que, se você alterar um objeto, apenas altere seu gráfico de dependência para baixo, e outros objetos que usam esse objeto não serão afetados por ele.
  3. Você pode estar criando e testando as coisas erradas . Seus objetos foram criados para facilitar a interface ou a implementação? Se for o último caso, você se verá alterando muito código que usa a interface da implementação antiga.
  4. Na melhor das hipóteses, siga rigorosamente o princípio de responsabilidade única. Na pior das hipóteses, siga o princípio de Segregação da interface. Consulte Princípios do SOLID .
Yam Marcovic
fonte
5
+1 paraDon't think of it as "fixing the tests", but as "defining new requirements".
StuperUser
2
+1 Os testes devem especificar sua aplicação primeiro, e não o contrário
treecoder
11

O que você descreve pode não ser tão ruim, mas um indicador de problemas mais profundos que seus testes descobrem

À medida que o sistema muda, nos vemos gastando mais tempo consertando testes quebrados. Temos testes unitários, de integração e funcionais.

Se você pudesse alterar seu código e seus testes não fossem interrompidos, isso seria suspeito para mim. A diferença entre uma mudança legítima e um bug é apenas o fato de ser solicitada, e o que é solicitado é (TDD assumido) definido pelos seus testes.

os dados foram codificados.

Dados codificados em testes são uma coisa boa. Os testes funcionam como falsificações, não como provas. Se houver muito cálculo, seus testes podem ser tautologias. Por exemplo:

assert sum([1,2,3]) == 6
assert sum([1,2,3]) == 1 + 2 + 3
assert sum([1,2,3]) == reduce(operator.add, [1,2,3])

Quanto maior a abstração, mais você se aproxima do algoritmo e, com isso, mais perto de comparar a implementação acutal a si mesma.

muito pouca reutilização de código

A melhor reutilização de código nos testes é imho 'Checks', como nas jUnits assertThat, porque eles mantêm os testes simples. Além disso, se os testes puderem ser refatorados para compartilhar código, o código real testado provavelmente também pode ser , reduzindo assim os testes àqueles que testam a base refatorada.

keppla
fonte
Eu gostaria de saber onde o infrator não concorda.
keppla
keppla - Eu não sou o menos favorável, mas geralmente, dependendo de onde estou no modelo, sou a favor de testar a interação do objeto em vez de testar os dados no nível da unidade. Testar dados funciona melhor em um nível de integração.
Ritch Melton
@keppla Tenho uma classe que direciona um pedido para um canal diferente se o total de itens contiver determinados itens restritos. Eu crio um pedido falso preenchendo-o com 4 itens, dois dos quais restritos. Na medida em que os itens restritos são adicionados, esse teste é exclusivo. Mas as etapas para criar um pedido falso e adicionar dois itens regulares são a mesma configuração que outro teste usa para testar o fluxo de trabalho de itens não restritos. Nesse caso, junto com os itens, se o pedido precisar ter a configuração de dados do cliente e a configuração de endereços, etc. Por que apenas afirmar a reutilização?
Asif Shiraz 30/10
6

Eu tive esse problema também. Minha abordagem aprimorada foi a seguinte:

  1. Não escreva testes de unidade, a menos que sejam a única maneira de testar alguma coisa.

    Estou totalmente preparado para admitir que os testes de unidade têm o menor custo de diagnóstico e tempo para correção. Isso os torna uma ferramenta valiosa. O problema é que, com o óbvio que sua milhagem pode variar, os testes de unidade geralmente são muito pequenos para merecer o custo de manutenção da massa do código. Eu escrevi um exemplo na parte inferior, dê uma olhada.

  2. Use asserções onde quer que sejam equivalentes ao teste de unidade para esse componente. As asserções têm a propriedade legal de que elas sempre são verificadas em qualquer compilação de depuração. Portanto, em vez de testar as restrições da classe "Employee" em uma unidade de testes separada, você está efetivamente testando a classe Employee em todos os casos de teste do sistema. As asserções também têm a boa propriedade de que não aumentam a massa do código tanto quanto os testes de unidade (que eventualmente requerem andaimes / zombarias / qualquer outra coisa).

    Antes que alguém me mate: as construções de produção não devem colidir com afirmações. Em vez disso, eles devem fazer logon no nível "Erro".

    Como precaução para alguém que ainda não pensou nisso, não afirme nada nas entradas do usuário ou da rede. É um grande erro ™.

    Nas minhas últimas bases de código, removi criteriosamente os testes de unidade sempre que vejo uma oportunidade óbvia de afirmações. Isso reduziu significativamente o custo de manutenção geral e me tornou uma pessoa muito mais feliz.

  3. Prefira testes de sistema / integração, implementando-os para todos os seus fluxos primários e experiências do usuário. Os casos de canto provavelmente não precisam estar aqui. Um teste do sistema verifica o comportamento no final do usuário executando todos os componentes. Por isso, um teste do sistema é necessariamente mais lento, então escreva os que importam (nem mais, nem menos) e você encontrará os problemas mais importantes. Os testes do sistema têm uma sobrecarga de manutenção muito baixa.

    É importante lembrar que, como você está usando asserções, cada teste do sistema executará algumas centenas de "testes de unidade" ao mesmo tempo. Você também tem certeza de que os mais importantes são executados várias vezes.

  4. Escreva APIs fortes que possam ser testadas funcionalmente. Os testes funcionais são desajeitados e (vamos ser sinceros) meio que sem sentido se sua API dificultar a verificação dos componentes funcionais por conta própria. Bom design de API a) simplifica as etapas de teste eb) gera afirmações claras e valiosas.

    O teste funcional é a coisa mais difícil de acertar, especialmente quando você tem componentes que se comunicam um para muitos ou (pior ainda, oh, Deus) muitos para muitos através das barreiras do processo. Quanto mais entradas e saídas conectadas a um único componente, mais difícil é o teste funcional, porque você precisa isolar um deles para realmente testar sua funcionalidade.


Na questão "não escreva testes de unidade", apresentarei um exemplo:

TEST(exception_thrown_on_null)
{
    InternalDataStructureType sink;
    ASSERT_THROWS(sink.consumeFrom(NULL), std::logic_error);
    try {
        sink.consumeFrom(NULL);
    } catch (const std::logic_error& e) {
        ASSERT(e.what() == "You must not pass NULL as a parameter!");
    }
}

O escritor deste teste adicionou sete linhas que não contribuem nada para a verificação do produto final. O usuário nunca deve ver isso acontecendo, porque a) ninguém deve passar NULL lá (então escreva uma afirmação) ou b) o caso NULL deve causar um comportamento diferente. Se o caso for (b), escreva um teste que realmente verifique esse comportamento.

Minha filosofia tornou-se que não devemos testar artefatos de implementação. Devemos apenas testar qualquer coisa que possa ser considerada uma saída real. Caso contrário, não há como evitar escrever duas vezes a massa básica de código entre os testes de unidade (que forçam uma implementação específica) e a própria implementação.

É importante observar, aqui, que existem bons candidatos para testes de unidade. De fato, existem até várias situações em que um teste de unidade é o único meio adequado para verificar algo e em que é de alto valor escrever e manter esses testes. Do topo da minha cabeça, esta lista inclui algoritmos não triviais, contêineres de dados expostos em uma API e código altamente otimizado que parece "complicado" (também conhecido como "o próximo cara provavelmente estragará tudo").

Meu conselho específico para você, então: comece a excluir testes de unidade criteriosamente conforme eles quebram, fazendo a si mesmo a pergunta: "isso é uma saída ou estou desperdiçando código?" Você provavelmente conseguirá reduzir o número de coisas que estão desperdiçando seu tempo.

Andres Jaan Tack
fonte
3
Preferir testes de sistema / integração - isso é incrivelmente ruim. Seu sistema chega ao ponto em que está usando esses testes (sloww!) Para testar as coisas que podem ser capturadas rapidamente no nível da unidade e leva horas para serem executadas porque você tem muitos testes semelhantes e lentos.
Ritch Melton
11
@RitchMelton Totalmente separado da discussão, parece que você precisa de um novo servidor de IC. O IC não deve se comportar assim.
Andres Jaan Tack
11
Um programa com falha (que é o que as asserções fazem) não deve matar seu executor de teste (CI). É por isso que você tem um corredor de teste; para que algo possa detectar e relatar essas falhas.
Andres Jaan Tack
11
As asserções no estilo 'Assert', somente para depuração, que eu conheço (não as asserções de teste), exibem uma caixa de diálogo que trava o IC porque está aguardando a interação do desenvolvedor.
Ritch Melton
11
Ah, isso explicaria muito sobre a nossa discordância. :) Estou me referindo a afirmações no estilo C. Só agora notei que esta é uma questão do .NET. cplusplus.com/reference/clibrary/cassert/assert
Andres Jaan Tack
5

Parece-me que o seu teste de unidade funciona como um encanto. É bom que seja tão frágil às mudanças, já que esse é o ponto principal. Pequenas alterações nos testes de quebra de código para que você possa eliminar a possibilidade de erro em todo o programa.

No entanto, lembre-se de que você realmente só precisa testar as condições que poderiam fazer com que seu método falhasse ou desse resultados inesperados. Isso manteria sua unidade testando mais propenso a "quebrar" se houver um problema genuíno, em vez de coisas triviais.

Embora me pareça que você está reprojetando fortemente o programa. Nesses casos, faça o que for necessário e remova os testes antigos e substitua-os por novos depois. Reparar testes de unidade só vale a pena se você não estiver consertando devido a mudanças radicais no seu programa. Caso contrário, você pode achar que está dedicando muito tempo para reescrever testes para ser aplicável na sua seção recém-escrita do código do programa.

Neil
fonte
3

Estou certo de que outras pessoas terão muito mais informações, mas, na minha experiência, estas são algumas coisas importantes que o ajudarão:

  1. Use uma fábrica de objetos de teste para criar estruturas de dados de entrada, para que você não precise duplicar essa lógica. Talvez procure uma biblioteca auxiliar, como o AutoFixture, para reduzir o código necessário para a configuração do teste.
  2. Para cada classe de teste, centralize a criação do SUT, para que seja fácil mudar quando as coisas forem refatoradas.
  3. Lembre-se de que o código de teste é tão importante quanto o código de produção. Também deve ser refatorado, se você achar que está se repetindo, se o código parecer insustentável, etc., etc.
driis
fonte
Quanto mais você reutiliza o código nos testes, mais frágeis eles se tornam, porque agora a alteração de um teste pode quebrar o outro. Esse pode ser um custo razoável, em troca da capacidade de manutenção - não estou entrando nesse argumento aqui -, mas argumentar que os pontos 1 e 2 tornam os testes menos frágeis (qual era a questão) é errado.
pdr
@driis - Certo, o código de teste tem idiomas diferentes do código em execução. Ocultar coisas refatorando código 'comum' e usando coisas como contêineres IoC apenas oculta problemas de design expostos por seus testes.
Ritch Melton
Embora o argumento que o @pdr defenda seja provavelmente válido para testes de unidade, eu argumentaria que, para testes de integração / sistema, pode ser útil pensar em termos de "preparar o aplicativo para a tarefa X". Isso pode envolver a navegação para o local apropriado, definir determinadas configurações de tempo de execução, abrir um arquivo de dados e assim por diante. Se vários testes de integração começarem no mesmo local, refatorar esse código para reutilizá-lo em vários testes pode não ser ruim se você entender os riscos e as limitações dessa abordagem.
um CVn
2

Manuseie testes como você faz com o código-fonte.

Controle de versão, lançamentos de pontos de verificação, rastreamento de problemas, "propriedade de recursos", planejamento e estimativa de esforços, etc. etc.

mosquito
fonte
1

Você definitivamente deve dar uma olhada nos padrões de teste XUnit de Gerard Meszaros . Possui uma ótima seção com muitas receitas para reutilizar seu código de teste e evitar duplicação.

Se seus testes são frágeis, também pode ser que você não recorra o suficiente para fazer o dobro. Especialmente, se você recriar gráficos inteiros de objetos no início de cada teste de unidade, as seções Organizar em seus testes poderão ficar grandes demais e você poderá se encontrar em situações em que precisará reescrever as seções Organizar em um número considerável de testes apenas porque uma das classes mais usadas foi alterada. Zombarias e stubs podem ajudá-lo aqui, reduzindo o número de objetos que você precisa reidratar para ter um contexto de teste relevante.

Tirar os detalhes sem importância de suas configurações de teste por meio de zombarias e stubs e aplicar padrões de teste para reutilizar o código deve reduzir significativamente sua fragilidade.

guillaume31
fonte