Projetando testes de unidade para um sistema com estado

20

fundo

O Desenvolvimento Orientado a Testes foi popularizado depois que eu já havia terminado a escola e o setor. Estou tentando aprender, mas algumas coisas importantes ainda me escapam. Os proponentes do TDD dizem muitas coisas como (doravante referido como o "princípio de asserção única" ou SAP ):

Há algum tempo que penso sobre como os testes TDD podem ser tão simples, expressivos e elegantes quanto possível. Este artigo explora um pouco sobre como é tornar os testes o mais simples e decompostos possível: visando uma única afirmação em cada teste.

Fonte: http://www.artima.com/weblogs/viewpost.jsp?thread=35578

Eles também dizem coisas assim (daqui em diante referido como "princípio do método privado" ou PMP ):

Geralmente, você não realiza testes particulares de unidade diretamente. Como eles são privados, considere-os um detalhe de implementação. Ninguém nunca ligará para um deles e espera que funcione de uma maneira particular.

Em vez disso, você deve testar sua interface pública. Se os métodos que chamam seus métodos particulares estiverem funcionando conforme o esperado, você supõe, por extensão, que seus métodos particulares estão funcionando corretamente.

Fonte: Como você faz testes de unidade em métodos particulares?

Situação

Estou tentando testar um sistema de processamento de dados com estado. O sistema pode fazer coisas diferentes para exatamente os mesmos dados, considerando seu estado antes de receber esses dados. Considere um teste direto que construa o estado no sistema e, em seguida, teste o comportamento que o método fornecido pretende testar.

  • A SAP sugere que eu não deveria testar o "procedimento de criação de estado", devo assumir que o estado é o que espero do código de criação e testar a mudança de estado que estou tentando testar

  • O PMP sugere que não posso pular essa etapa de "construção do estado" e apenas testar os métodos que governam essa funcionalidade de forma independente.

O resultado no meu código real foram testes inchados, complicados, longos e difíceis de escrever. E se as transições de estado mudarem, os testes precisam ser alterados ... o que seria bom com testes pequenos e eficientes, mas que consumiam muito tempo e confundiam esses testes inchados. Como isso é feito normalmente?

durron597
fonte
2
Eu não acho que você encontrará uma solução elegante para isso. A abordagem geral não é tornar o sistema com estado, para começar, o que não ajuda no teste de algo que já foi construído. Refatorar para que seja apátrida provavelmente também não vale o custo.
Doval
@Doval: Por favor, explique como tornar algo como um telefone (SIP UserAgent) não estatal. O comportamento esperado desta unidade é especificado no RFC usando um diagrama de transição de estado.
Bart van Ingen Schenau
Você está copiando / colando / editando seus testes ou está escrevendo métodos utilitários para compartilhar configurações / desmontagens / funcionalidades comuns? Embora alguns casos de teste possam certamente ficar longos e inchados, isso não deve ser tão comum. Em um sistema com estado, eu esperaria uma rotina de configuração comum em que o estado final seja um parâmetro e essa rotina leve você ao estado que deseja testar. Além disso, no final de cada teste, eu teria um método de desmontagem que o levará de volta ao estado inicial conhecido (se necessário), para que seu método de configuração funcione corretamente quando o próximo teste começar.
Dunk
Em uma tangente, mas também acrescentarei que os diagramas de estado são uma ferramenta de comunicação e não um decreto de implementação, mesmo que esteja em uma RFC. Contanto que você atenda à funcionalidade descrita, atenda ao padrão. Já tive algumas ocasiões em que converti implementações de transição de estado realmente complicadas (conforme definidas nos RFCs) em uma funcionalidade de processamento geral realmente simples. Em um caso, lembro-me de me livrar de algumas milhares de linhas de código quando percebi que, além de algumas sinalizações, cerca de cinco estados fizeram exatamente a mesma coisa quando você renomeou os elementos comuns "ocultos".
Dunk

Respostas:

15

Perspectiva:

Então, vamos dar um passo atrás e perguntar o que o TDD está tentando nos ajudar. O TDD está tentando nos ajudar a determinar se nosso código está correto ou não. E por correto, quero dizer "o código atende aos requisitos de negócios?" O ponto de venda é que sabemos que mudanças serão necessárias no futuro e queremos garantir que nosso código permaneça correto depois de fazer essas alterações.

Trago essa perspectiva porque acho que é fácil nos perder nos detalhes e perder de vista o que estamos tentando alcançar.

Princípios - SAP:

Embora eu não seja um especialista em TDD, acho que você está perdendo parte do que o SAP (Single Assertion Principle) está tentando ensinar. O SAP pode ser atualizado como "teste uma coisa de cada vez". Mas o TOTAT não sai da língua tão facilmente quanto o SAP.

Testar uma coisa de cada vez significa que você se concentra em um caso; um caminho; uma condição de contorno; um caso de erro; um qualquer por teste. E a idéia principal por trás disso é que você precisa saber o que quebrou quando o caso de teste falha, para poder resolver o problema mais rapidamente. Se você testar várias condições (ou seja, mais de uma coisa) dentro de um teste e o teste falhar, você terá muito mais trabalho em mãos. Primeiro você precisa identificar quais dos vários casos falharam e depois descobrir por que esse caso falhou.

Se você testar uma coisa de cada vez, seu escopo de pesquisa é muito menor e o defeito é identificado mais rapidamente. Lembre-se de que "testar uma coisa de cada vez" não necessariamente o impede de observar mais de uma saída do processo por vez. Por exemplo, ao testar um "bom caminho conhecido", posso esperar ver um valor específico resultante foo, assim como outro valor, bare posso verificar isso foo != barcomo parte do meu teste. A chave é agrupar logicamente as verificações de saída com base no caso que está sendo testado.

Princípios - PMP:

Da mesma forma, acho que está faltando um pouco sobre o que o PMP (Private Method Principio) tem a nos ensinar. O PMP nos encoraja a tratar o sistema como uma caixa preta. Para uma determinada entrada, você deve obter uma determinada saída. Você não se importa como a caixa preta gera a saída. Você só se importa que suas saídas estejam alinhadas com suas entradas.

O PMP é realmente uma boa perspectiva para examinar os aspectos da API do seu código. Também pode ajudá-lo a definir o que você precisa testar. Identifique seus pontos de interface e verifique se eles atendem aos termos de seus contratos. Você não precisa se preocupar com a maneira como os métodos por trás da interface (também conhecidos como privados) fazem seu trabalho. Você só precisa verificar se eles fizeram o que deveriam fazer.


TDD aplicado ( para você )

Portanto, sua situação apresenta um pouco de rugas além de um aplicativo comum. Os métodos do seu aplicativo são válidos, portanto, a saída deles depende não apenas da entrada, mas também do que foi feito anteriormente. Tenho certeza de que devo <insert some lecture>dizer aqui que o estado é horrível e blá blá blá, mas isso realmente não ajuda a resolver seu problema.

Eu vou assumir que você tem algum tipo de tabela de diagrama de estados que mostra os vários estados potenciais e o que precisa ser feito para desencadear uma transição. Caso contrário, será necessário, pois ajudará a expressar os requisitos de negócios para esse sistema.

Os testes: primeiro, você terminará com um conjunto de testes que promovem a mudança de estado. Idealmente, você terá testes que exercitam toda a gama de alterações de estado que podem ocorrer, mas posso ver alguns cenários em que talvez você não precise ir nessa extensão completa.

Em seguida, você precisa criar testes para validar o processamento de dados. Alguns desses testes de estado serão reutilizados quando você cria os testes de processamento de dados. Por exemplo, suponha que você tenha um método Foo()que possua saídas diferentes com base nos estados Inite State1. Você desejará usar seu ChangeFooToState1teste como uma etapa de configuração para testar a saída quando " Foo()estiver dentro State1".

Há algumas implicações por trás dessa abordagem que quero mencionar. Spoiler, é aqui que eu vou enfurecer os puristas

Primeiro, você precisa aceitar que está usando algo como teste em uma situação e configuração em outra. Por um lado, isso parece ser uma violação direta do SAP. Mas se você definir logicamente ChangeFooToState1como tendo dois propósitos, ainda estará conhecendo o espírito do que a SAP está nos ensinando. Quando você precisar garantir que as Foo()alterações sejam feitas, use-o ChangeFooToState1como teste. E quando precisar validar Foo()a saída de State1"quando estiver", você estará usando ChangeFooToState1como configuração.

O segundo item é que, do ponto de vista prático, você não desejará testes de unidade totalmente aleatórios para o seu sistema. Você deve executar todos os testes de alteração de estado antes de executar os testes de validação de saída. SAP é o tipo de princípio orientador por trás desse pedido. Para declarar o que deveria ser óbvio - você não pode usar algo como configuração se ele falhar como teste.

Juntar as peças:

Usando seu diagrama de estado, você gerará testes para cobrir as transições. Novamente, usando seu diagrama, você gera testes para cobrir todos os casos de processamento de dados de entrada / saída conduzidos por estado.

Se você seguir essa abordagem, os bloated, complicated, long, and difficult to writetestes deverão ficar um pouco mais fáceis de gerenciar. Em geral, eles devem acabar menores e devem ser mais concisos (isto é, menos complicados). Você deve observar que os testes também são mais dissociados ou modulares.

Agora, não estou dizendo que o processo será completamente indolor, porque escrever bons testes exige algum esforço. E alguns deles ainda serão difíceis porque você está mapeando um segundo parâmetro (estado) em vários de seus casos. E, como um aparte, deve ser um pouco mais evidente por que um sistema sem estado é mais fácil de construir testes. Mas se você adaptar essa abordagem para o seu aplicativo, deverá descobrir que é capaz de provar que seu aplicativo está funcionando corretamente.


fonte
11

Você geralmente abstrai os detalhes da configuração em funções para não precisar se repetir. Dessa forma, você só precisará alterá-lo em um local no teste se a funcionalidade mudar.

No entanto, você normalmente não gostaria de descrever nem mesmo as funções de configuração como inchadas, complicadas ou longas. Isso é um sinal de que sua interface precisa ser refatorada, porque, se for difícil para seus testes, também é difícil para seu código real.

Isso geralmente é um sinal de colocar muito em uma classe. Se você tiver requisitos com estado, precisará de uma classe que gerencie o estado e nada mais. As classes que o suportam devem ser apátridas. Para o seu exemplo SIP, a análise de um pacote deve ser completamente sem estado. Você pode ter uma classe que analisa um pacote e depois chama algo como sipStateController.receiveInvite()gerenciar as transições de estado, que chama outras classes sem estado para fazer coisas como tocar o telefone.

Isso torna a configuração do teste de unidade para a classe de máquina de estado uma questão simples de algumas chamadas de método. Se sua configuração para testes de unidade de máquina de estado exigir a criação de pacotes, você investiu demais nessa classe. Da mesma forma, sua classe de analisador de pacotes deve ser relativamente simples de criar código de instalação, usando um mock para a classe de máquina de estado.

Em outras palavras, você não pode evitar o estado completamente, mas pode minimizá-lo e isolá-lo.

Karl Bielefeldt
fonte
Apenas para constar, o exemplo do SIP era meu, não do OP. E algumas máquinas de estado podem precisar de mais do que algumas chamadas de método para colocá-las no estado certo para um determinado teste.
Bart van Ingen Schenau
+1 em "você não pode evitar o estado completamente, mas pode minimizá-lo e isolá-lo". Eu não pude concordar. O estado é um mal necessário no software.
Brandon
0

A idéia central do TDD é que, escrevendo os testes primeiro, você acaba com um sistema que é, no mínimo, fácil de testar. Espero que funcione, seja sustentável, bem documentado e assim por diante, mas, se não, bem, pelo menos ainda é fácil testar.

Então, se você TDD e acaba com um sistema difícil de testar, algo deu errado. Talvez algumas coisas privadas devam ser públicas, porque você precisa delas para testes. Talvez você não esteja trabalhando no nível certo de abstração; algo tão simples como uma lista é estável em um nível, mas um valor em outro. Ou talvez você esteja dando muito peso a conselhos não aplicáveis ​​em seu contexto, ou seu problema é apenas difícil. Ou, é claro, talvez seu design seja apenas ruim.

Seja qual for a causa, você provavelmente não voltará e gravará seu sistema novamente para torná-lo mais testável com um código de teste simples. Provavelmente, o melhor plano é usar algumas técnicas de teste um pouco mais sofisticadas, como:

soru
fonte