Estou tentando entender os testes de unidade.
Digamos que temos um dado que pode ter um número padrão de lados igual a 6 (mas pode ter 4, 5 lados, etc.):
import random
class Die():
def __init__(self, sides=6):
self._sides = sides
def roll(self):
return random.randint(1, self._sides)
Seriam os seguintes testes de unidade válidos / úteis?
- teste um rolo no intervalo de 1 a 6 para um dado de 6 lados
- teste um rolo de 0 para um dado de 6 lados
- teste um rolo de 7 para um dado de 6 lados
- teste um rolo na faixa de 1-3 para um dado de 3 lados
- teste um rolo de 0 para um dado de 3 lados
- teste um rolo de 4 para um dado de 3 lados
Eu só estou pensando que isso é uma perda de tempo, já que o módulo aleatório existe há bastante tempo, mas acho que se o módulo aleatório for atualizado (digamos, eu atualizei minha versão do Python), pelo menos estou coberto.
Além disso, eu preciso testar outras variações de rolos de matriz, por exemplo, os 3 neste caso, ou é bom cobrir outro estado de matriz inicializado?
python
unit-testing
tdd
Cybran
fonte
fonte
Respostas:
Você está certo, seus testes não devem verificar se o
random
módulo está fazendo seu trabalho; um unittest deve testar apenas a classe em si, não como ele interage com outro código (que deve ser testado separadamente).É claro que é perfeitamente possível que seu código use
random.randint()
errado; ou você está ligandorandom.randrange(1, self._sides)
e seu dado nunca gera o valor mais alto, mas esse seria um tipo diferente de inseto, que você não poderia pegar com um pouco mais unido. Nesse caso, suadie
unidade está funcionando como projetada, mas o design em si foi defeituoso.Nesse caso, eu usaria zombaria para substituir a
randint()
função e verifique apenas se ela foi chamada corretamente. O Python 3.3 e superior vem com ounittest.mock
módulo para lidar com esse tipo de teste, mas você pode instalar omock
pacote externo em versões mais antigas para obter exatamente a mesma funcionalidadeCom a zombaria, seu teste agora é muito simples; existem apenas 2 casos, realmente. A caixa padrão para uma matriz de 6 lados e a caixa dos lados personalizados.
Existem outras maneiras de substituir temporariamente a
randint()
função no espaço para nome global deDie
, mas omock
módulo facilita isso. O@mock.patch
decorador aqui se aplica a todos os métodos de teste no caso de teste; cada método de teste recebe um argumento extra, arandom.randint()
função de simulação, para que possamos testar contra a simulação para ver se ela realmente foi chamada corretamente. Oreturn_value
argumento especifica o que é retornado do mock quando é chamado, para que possamos verificar se odie.roll()
método realmente retornou o resultado 'aleatório' para nós.Eu usei outra prática recomendada sem Python aqui: importe a classe em teste como parte do teste. O
_make_one
método faz a importação e a instanciação dentro de um teste , para que o módulo de teste ainda seja carregado, mesmo se você cometer um erro de sintaxe ou outro erro que impeça a importação do módulo original.Dessa forma, se você cometeu um erro no próprio código do módulo, os testes ainda serão executados; eles simplesmente falharão, informando sobre o erro no seu código.
Para deixar claro, os testes acima são simplistas ao extremo. O objetivo aqui não é testar o que
random.randint()
foi chamado com os argumentos corretos, por exemplo. Em vez disso, o objetivo é testar se a unidade está produzindo os resultados certos, com base em determinadas entradas, onde essas entradas incluem os resultados de outras unidades que não estão sendo testadas. Ao zombar dorandom.randint()
método, você assume o controle sobre apenas outra entrada do seu código.Nos testes do mundo real , o código real em sua unidade em teste será mais complexo; o relacionamento com as entradas passadas para a API e como outras unidades são chamadas ainda pode ser interessante ainda, e a zombaria lhe dará acesso a resultados intermediários, além de definir os valores de retorno para essas chamadas.
Por exemplo, no código que autentica os usuários em relação a um serviço OAuth2 de terceiros (uma interação de vários estágios), você deseja testar se seu código está passando os dados corretos para esse serviço de terceiros e simular respostas de erro diferentes que O serviço de terceiros retornaria, permitindo simular diferentes cenários sem a necessidade de criar um servidor OAuth2 completo. Aqui é importante testar se as informações de uma primeira resposta foram tratadas corretamente e foram passadas para uma chamada de segundo estágio, para que você queira ver se o serviço simulado está sendo chamado corretamente.
fonte
randint()
, e não o códigoDie.roll()
.sentinel.die
exemplo (o objeto sentinelaunittest.mock
também é) e verifique se foi o que foi retornado pelo seu método de rolagem. Na verdade, isso permite apenas uma maneira de implementar o método testado.sentinel.die
seria uma ótima maneira de garantir isso.A resposta de Martijn é como você faria se realmente quisesse executar um teste que demonstrasse que você está chamando random.randint. No entanto, correndo o risco de ser informado "que não responde à pergunta", acho que isso não deveria ser testado em unidade. Mocking randint não é mais teste de caixa preta - você está mostrando especificamente que certas coisas estão acontecendo na implementação . O teste da caixa preta não é nem uma opção - não há nenhum teste que você possa executar que prove que o resultado nunca será menor que 1 ou mais que 6.
Você pode zombar
randint
? Sim você pode. Mas o que você está provando? Você o chamou com argumentos 1 e lados. O que isso significa? Você está de volta à estaca zero - no final do dia, terá que provar ( formal ou informalmente) que o chamadorandom.randint(1, sides)
corretamente implementa uma jogada de dados.Sou a favor de testes de unidade. São verificações fantásticas de sanidade e expõem a presença de bugs. No entanto, eles nunca podem provar sua ausência, e há coisas que não podem ser afirmadas por meio de testes (por exemplo, que uma função específica nunca lança uma exceção ou sempre termina.) Nesse caso específico, sinto que há muito pouco a fazer. ganho. Para um comportamento determinístico, os testes de unidade fazem sentido porque você realmente sabe qual será a resposta que espera.
fonte
random.randint
chamado1, sides
é inútil se for a coisa errada a fazer.random.randint()
retorne corretamente valores no intervalo [1, lados] (inclusive), isso depende dos desenvolvedores do Python para garantir que arandom
unidade funcione corretamente.random.randint()
se comportar comorandom.randrange()
e assim chamá-lo comrandom.randint(1, sides + 1)
, então você está afundado de qualquer maneira.Corrija a semente aleatória. Para dados de 1, 2, 5 e 12 faces, confirme que alguns milhares de lançamentos apresentam resultados incluindo 1 e N, e não incluindo 0 ou N + 1. Se por acaso, você obtém um conjunto de resultados aleatórios que não cobrir o intervalo esperado, mude para uma semente diferente.
As ferramentas de zombaria são legais, mas apenas porque permitem que você faça uma coisa não significa que ela deve ser feita. O YAGNI se aplica tanto a dispositivos de teste quanto a recursos.
Se você pode testar facilmente com dependências desassistidas, você sempre deve; Dessa forma, seus testes serão focados na redução da contagem de defeitos, e não apenas no aumento da contagem de testes. O risco de zombaria excessivo cria números de cobertura enganosos, o que por sua vez pode levar ao adiamento do teste real para uma fase posterior. Talvez você nunca tenha tempo para ...
fonte
O que é um
Die
se você pensar sobre isso? - não mais do que um invólucrorandom
. Ele encapsularandom.randint
e relabels em termos de vocabulário próprio da sua aplicação:Die.Roll
.Não acho relevante inserir outra camada de abstração entre
Die
erandom
porqueDie
ela já é essa camada de indireção entre seu aplicativo e a plataforma.Se você deseja resultados de dados enlatados, apenas zombe
Die
, não zomberandom
.Em geral, eu não teste de unidade meus objetos de invólucro que se comunicam com sistemas externos, escrevo testes de integração para eles. Você pode escrever alguns deles,
Die
mas, como você apontou, devido à natureza aleatória do objeto subjacente, eles não serão significativos. Além disso, não há configuração ou comunicação de rede envolvida aqui, portanto não há muito a ser testado, exceto uma chamada de plataforma.=> Considerando que
Die
são apenas algumas linhas triviais de código e adiciona pouca ou nenhuma lógica em relação arandom
si mesma, eu pularia o teste nesse exemplo específico.fonte
Semear o gerador de números aleatórios e verificar os resultados esperados NÃO é, até onde posso ver, um teste válido. Ele faz suposições sobre como seus dados funcionam internamente, o que é impertinente. Os desenvolvedores do python podem alterar o gerador de números aleatórios ou o dado (NOTA: "dados" é plural, "dado" é singular. A menos que sua classe implemente várias rolagens de dados em uma chamada, provavelmente deve ser chamado de "dado") use um gerador de números aleatórios diferente.
Da mesma forma, zombar da função aleatória pressupõe que a implementação da classe funcione exatamente como o esperado. Por que não pode ser esse o caso? Alguém pode assumir o controle do gerador de números aleatórios python padrão e, para evitar isso, uma versão futura do seu dado pode buscar vários números aleatórios, ou números aleatórios maiores, para misturar mais dados aleatórios. Um esquema semelhante foi usado pelos fabricantes do sistema operacional FreeBSD, quando suspeitaram que a NSA estava adulterando os geradores de números aleatórios de hardware embutidos nas CPUs.
Se fosse eu, eu executaria, digamos, 6000 rolagens, as contaria e garantiria que cada número de 1 a 6 fosse rolado entre 500 e 1500 vezes. Eu também verificaria se nenhum número fora desse intervalo é retornado. Também posso verificar que, para um segundo conjunto de 6000 rolos, ao solicitar o [1..6] em ordem de frequência, o resultado é diferente (isso falhará uma vez em 720 execuções, se os números forem aleatórios!). Se você quiser ser cuidadoso, poderá encontrar a frequência dos números após 1, após 2, etc; mas verifique se o tamanho da amostra é grande o suficiente e se há variação suficiente. Os seres humanos esperam que números aleatórios tenham menos padrões do que realmente têm.
Repita o procedimento para um dado de 12 e 2 lados (6 é o mais usado, assim como o mais esperado para quem escreve esse código).
Finalmente, eu testaria para ver o que acontece com um dado de um lado, um dado de 0 lado, um dado de 1 lado, um dado de 2,3 lados, um dado de [1,2,3,4,5,6] e um dado do tipo "blá". Claro, tudo isso deve falhar; eles falham de uma maneira útil? Provavelmente, isso deve falhar na criação, não na rolagem.
Ou, talvez, você também queira lidar com isso de maneira diferente - talvez criar um dado com [1,2,3,4,5,6] deva ser aceitável - e talvez "blá" também; pode ser um dado com 4 faces, e cada face com uma letra. O jogo "Boggle" vem à mente, assim como uma bola mágica de oito.
E, finalmente, você pode considerar isso: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg
fonte
Correndo o risco de nadar contra a maré, resolvi esse problema exato alguns anos atrás, usando um método ainda não mencionado.
Minha estratégia era simplesmente zombar do RNG com um que produza um fluxo previsível de valores que abrange todo o espaço. Se (digamos) lado = 6 e o RNG produzir valores de 0 a 5 em sequência, posso prever como minha classe deve se comportar e o teste de unidade em conformidade.
A lógica é que isso testa a lógica apenas nesta classe, supondo que o RNG acabará por produzir cada um desses valores e sem testar o próprio RNG.
É simples, determinístico, reproduzível e captura bugs. Eu usaria a mesma estratégia novamente.
A questão não especifica quais devem ser os testes, apenas quais dados podem ser usados para os testes, dada a presença de um RNG. Minha sugestão é apenas testar exaustivamente zombando do RNG. A questão do que vale a pena testar depende das informações não fornecidas na pergunta.
fonte
Os testes sugeridos em sua pergunta não detectam um contador aritmético modular como implementação. E eles não detectam erros comuns de implementação em códigos relacionados à distribuição de probabilidade
return 1 + (random.randint(1,maxint) % sides)
. Ou uma alteração no gerador que resulta em padrões bidimensionais.Se você realmente deseja verificar se está gerando números de aparência aleatória distribuídos uniformemente, precisa verificar uma variedade muito ampla de propriedades. Para fazer um trabalho razoavelmente bom, você pode executar http://www.phy.duke.edu/~rgb/General/dieharder.php nos números gerados. Ou escreva um conjunto de testes de unidade igualmente complexo.
Isso não é culpa do teste de unidade ou do TDD, a aleatoriedade é uma propriedade muito difícil de verificar. E um tópico popular para exemplos.
fonte
O teste mais fácil de um rolo de molde é simplesmente repeti-lo várias centenas de milhares de vezes e validar que cada resultado possível foi atingido aproximadamente (1 / número de lados) vezes. No caso de um dado de 6 lados, você deve ver cada valor possível atingido em 16,6% das vezes. Se houver mais de um por cento de desconto, você tem um problema.
Fazer isso dessa maneira evita que você refatorar o mecânico subjacente de gerar um número aleatório com facilidade e, o mais importante, sem alterar o teste.
fonte