Criando testes com Hypothesis

Overview da library para criar unit tests poderosos em Python

Publicado por Lohan Bodevan 25 de Outubro de 2016 às 12:28

Introdução

Um dia desses precisei criar testes unitários que contemplassem uma grande variedade de inputs. Em suma, meu teste precisava simular que o usuário pudesse informar ao sistema diferentes números inteiros de valor alto, baixo, positivo ou negativo.

Foi aí que a virtuosa preguiça de desenvolvedor me fez mergulhar no Google, até que encontrei o Hypothesis. Essa biblioteca ajuda a criar casos de testes que nem sequer pensamos, de uma forma bem simples.

Vamos à um exemplo prático

Suponha que precisamos testar uma função simples que tem por objetivo somar dois inteiros. Em Python essa função pode ser escrita da seguinte forma:

def sum_numbers(a, b):
    return a + b

Como faríamos um unittest para essa função?

import unittest

from example import sum_numbers


class TestSumNumber(unittest.TestCase):

   def test_ints_sum(self):
       x = 2
       y = 2
       assert x + y == sum_numbers(x, y)


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

Ok! É um teste válido, mas será que só isso cobre diferentes possibilidades de inteiros. Podemos então criar um novo teste para números inteiros negativos:

def test_sum_negative_integer(self):
    x = -2
    y = 2
    assert x + y == sum_numbers(x, y)

Também é válido mas, e os extensos números inteiros? Tal como 11287984639472397.

Perceba que uma simples função de somar pode ter diversas possibilidades de entrada e é dever do seu teste garantir que ela responderá igual para todas. Então como aumentar essa cobertura? Vamos ver como ficaria esse mesmo teste com o Hypothesis:

import unittest

from hypothesis import given
import hypothesis.strategies as st

from example import sum_numbers


class TestSumNumbers(unittest.TestCase):

   @given(st.integers(), st.integers())
   def test_ints_sum(self, x, y):
       assert x + y == sum_numbers(x, y)


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

Simples, não? O decorator @given vai injetar parâmetros no nosso teste que são determinados pela expressão "st.integers()".

Aí você deve estar pensando: "Legal, a cada vez que eu rodar meu teste o Hypothesis vai fornecer diferentes inteiros. Mas, pode ser que quando eu rode o teste uma vez minha função "sum_numbers" passe nos testes e em outra ocasião não."

Aí que está a beleza do Hypotesis, esse único teste produz diversas entradas. Vamos modificar nosso teste para "logar" os parâmetros que estão sendo recebidos:

import logging
import unittest

from hypothesis import given
import hypothesis.strategies as st

from example import sum_numbers


logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger('HYPOTHESIS')


class TestSumNumbers(unittest.TestCase):

   @given(st.integers(), st.integers())
   def test_ints_sum(self, x, y):
       log.info('Testing {} plus {}'.format(x, y))

       assert x + y == sum_numbers(x, y)


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

Então se eu rodar agora esse teste ele deve produzir a seguinte linha no console:

INFO:HYPOTHESIS:Testing x plus y

Onde x e y serão injetados pelo Hypothesis. Porém, caro amigo, a saída será algo parecido com a imagem abaixo:

Resultado de Testes com Hypotesis

Vai me dizer que não dá muito mais segurança agora sabendo que sua função "sum_numbers" somou corretamente todas essas possibilidades de inteiros?

E o Hypotesis não para por aí, você pode aplicar essa técnica para strings, listas e etc. Recomendo a leitura da documentação aqui, você vai encontrar muitas formas de melhorar a cobertura dos seus testes.

Essa "técnica" (se é que podemos chamar assim) de escrever testes não é nova, ela vem lá de 1999 criada inicialmente para linguagem Haskell e hoje já possui implementações para muitas linguagens. Você pode encontrar nesse link as implementações disponíveis.

Como sempre, esse exemplo que apresentei aqui está no meu Github para consulta.

Abraço!


Comentários