Quais são os bons testes de unidade para cobrir o caso de uso de rolar um dado?

18

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?

Cybran
fonte
1
Que tal um dado com menos de 5 lados ou um dado com nulo?
JensG

Respostas:

22

Você está certo, seus testes não devem verificar se o randommó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á ligando random.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, sua die 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 o unittest.mockmódulo para lidar com esse tipo de teste, mas você pode instalar o mockpacote externo em versões mais antigas para obter exatamente a mesma funcionalidade

import unittest
try:
    from unittest.mock import patch
except ImportError:
    # < python 3.3
    from mock import patch


@patch('random.randint', return_value=3)
class TestDice(unittest.TestCase):
    def _make_one(self, *args, **kw):
        from die import Die
        return Die(*args, **kw)

    def test_standard_size(self, mocked_randint):
        die = self._make_one()
        result = die.roll()

        mocked_randint.assert_called_with(1, 6)
        self.assertEqual(result, 3)

    def test_custom_size(self, mocked_randint):
        die = self._make_one(sides=42)
        result = die.roll()

        mocked_randint.assert_called_with(1, 42)
        self.assertEqual(result, 3)


if __name__ == '__main__':
    unittest.main()

Com 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 de Die, mas o mockmódulo facilita isso. O @mock.patchdecorador aqui se aplica a todos os métodos de teste no caso de teste; cada método de teste recebe um argumento extra, a random.randint()função de simulação, para que possamos testar contra a simulação para ver se ela realmente foi chamada corretamente. O return_valueargumento especifica o que é retornado do mock quando é chamado, para que possamos verificar se o die.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_onemé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 do random.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.

Martijn Pieters
fonte
1
Você tem mais de dois casos de teste ... os resultados verificam o valor padrão: inferior (1), superior (6), abaixo inferior (0), além de superior (7) e resultados para números especificados pelo usuário, como max_int input etc. também não é validado, que podem precisar de ser testado para em algum momento ...
James Snell
2
Não, esses são testes para randint(), e não o código Die.roll().
Martijn Pieters
Na verdade, existe uma maneira de garantir que não apenas o randint seja chamado corretamente, mas que o resultado também seja usado corretamente: faça uma simulação para retornar um sentinel.dieexemplo (o objeto sentinela unittest.mocktambé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.
Aragaer # 24/14
@aragaer: claro, se você deseja verificar se o valor é retornado inalterado, sentinel.dieseria uma ótima maneira de garantir isso.
Martijn Pieters
Não entendo por que você deseja garantir que mocked_randint seja chamado_ com determinados valores. Eu entendo que querer zombar do randint para retornar valores previsíveis, mas não é a preocupação apenas que ele retorna valores previsíveis e não com quais valores é chamado? Parece-me que a verificação dos valores chamados está desnecessariamente vinculando o teste aos mínimos detalhes da implementação. Também por que nos importamos que o dado retorne o valor exato de randint? Nós realmente não nos importamos apenas que ele retorne um valor> 1 e menor que igual ao máximo?
bdrx
16

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 chamado random.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.

Doval
fonte
Testes de unidade não são testes de caixa preta, na verdade. É para isso que servem os testes de integração, para garantir que as várias partes interajam como projetadas. É uma questão de opinião, é claro (a maioria da filosofia de teste é), consulte O "Teste de Unidade" se enquadra nos testes de caixa branca ou caixa preta? e Black Box Unit Testing para algumas perspectivas (Stack Overflow).
Martijn Pieters
@MartijnPieters Não concordo que "é para isso que servem os testes de integração". Os testes de integração são para verificar se todos os componentes do sistema interagem corretamente. Eles não são o lugar para testar se um determinado componente fornece a saída correta para uma determinada entrada. Quanto aos testes de unidade de caixa preta versus caixa branca, os testes de unidade da caixa branca acabarão quebrando com as alterações na implementação, e quaisquer suposições feitas na implementação provavelmente serão transferidas para o teste. Validar o random.randintchamado 1, sidesé inútil se for a coisa errada a fazer.
Doval
Sim, isso é uma limitação de um teste de unidade de caixa branca. No entanto, não há nenhum ponto no teste que random.randint()retorne corretamente valores no intervalo [1, lados] (inclusive), isso depende dos desenvolvedores do Python para garantir que a randomunidade funcione corretamente.
Martijn Pieters
E, como você diz, o teste de unidade não pode garantir que seu código esteja livre de erros; se seu código é usando outras unidades de forma errada (dizer, você espera random.randint()se comportar como random.randrange()e assim chamá-lo com random.randint(1, sides + 1), então você está afundado de qualquer maneira.
Martijn Pieters
2
@MartijnPieters Eu concordo com você lá, mas não é isso que estou objetando. Estou objetando a testar que random.randint esteja sendo chamado com argumentos (1, lados) . Você assumiu na implementação que esta é a coisa correta a fazer e agora está repetindo essa suposição no teste. Caso essa suposição esteja errada, o teste será aprovado, mas sua implementação ainda está incorreta. É uma prova meia-boca que é um saco cheio para escrever e manter.
Doval 24/03
6

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

soru
fonte
3

O que é um Diese você pensar sobre isso? - não mais do que um invólucro random. Ele encapsula random.randinte 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 Diee randomporque Dieela 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, Diemas, 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 Diesão apenas algumas linhas triviais de código e adiciona pouca ou nenhuma lógica em relação a randomsi mesma, eu pularia o teste nesse exemplo específico.

guillaume31
fonte
2

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

AMADANON Inc.
fonte
2

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.

david.pfx
fonte
Digamos que você zomba do RNG para ser previsível. Bem, o que você então testa? A pergunta é "Os seguintes testes de unidade são válidos / úteis?" Zombar dele para retornar 0-5 não é um teste, mas uma configuração de teste. Como você "testaria a unidade de acordo"? Não estou conseguindo entender como "captura bugs". Estou tendo dificuldade para entender o que preciso para o teste 'unitário'.
bdrx
@bdrx: Isso foi há um tempo atrás: eu responderia de forma diferente agora. Mas veja editar.
David.pfx # 11/14
1

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.

Patrick
fonte
-1

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.

ChristopherBrown
fonte
1
este teste iria passar para uma aplicação totalmente não-aleatória que simplesmente circula lados, um por um numa ordem pré-definida
mosquito
1
Se um codificador tem a intenção de implementar algo de má fé (não usar um agente aleatório em um dado) e simplesmente tentar encontrar algo para 'fazer as luzes vermelhas ficarem verdes', você terá mais problemas do que o teste de unidade pode realmente resolver.
ChristopherBrown