Python Mock com unittest

Posted on Seg 03 Outubro 2016 in python

Gosto do módulo "UnitTest" por ser um "buil-in", ou seja, porque ele já faz parte do Python. Ao usar o "unittest" não preciso aumentar as dependências do meu projeto. Claro, se já estou usando um "framework web" por exemplo e este depende de uma biblioteca de testes outra que não unittest, talvez eu considere usá-la. Diminuir a árvore de dependências é sempre bom !

Então, porque não usar `UnitTest` ?

O que é "mock"

"Mock" lhe permite criar situações que imitam o comportamento do seu software.

Através de objetos "Mock" é possível simular o comportamento de um objeto ou de uma função.

Mock = e uma imitação, geralmente de menor qualidade.

Então "mockar" é simular, adicionar uma comportamento que imita o real mas que de fato não é.

Qual a utilidade disso ?

Testes !!

Mock tem a ver com testes, mais especificamente testes unitários.

Teste unitários por definição testam uma unidade do código uma função. E é aí que o objeto Mock entra.

E pense no seguinte caso:

"Escrever um teste para um método de uma classe que recebe um objeto de um determinado tipo"

algo como ...

class Klasse():
    '''
    suposta classe complexa que não será testada, mas que recebe vários argumentos e complexo.
    '''
    def __init__(self, param1, param2 ... paramN):
        ...

    def hello():
        print ("oi")

def method_externo(kls):
    '''
    função a ser testada
    '''
    obj = kls(self, param1, param2 ... paramN)

    obj.hello()

Dado o caso acima, imagina que eu somente quero testar se minha função está chamando o método::hello() do objeto recebido. Mas para criar uma instancia do objeto obj::Klasse preciso antes definir uma dezena de parâmetros...

Eu posso economizar bastante tempo ao escrever meus testes, se eu simplesmente crio um objeto qualquer e adiciono um método::Hello() simplificado, somente para passar pra a função a qual eu quero realmente testar.

Eu posso fazer isso com Python puro, mas eete exemplo é muito simples, e podemos ter situações bem mais complexas que exijam outros comportamentos do meu objeto de apoio ao teste e voltamos a gastar tempo somente para "enganar" o sistema. E para resolver isso temos o Mock !

Um outro exemplo comum, só para exemplificarmos o poder do Mock, seria acessar uma API externa, onde eu posso "mockar" os objetos de resposta desta API para testar minha lógica, afinal não vou reconstruir o sistema da API somente para grantir os testes da minha aplicação.

O objeto Mock !

unittest.mock gera objetos que imitam qualquer coisa.

O código abaixo tem uma classe Python chamada MinhaClasse, e tem um método que sempre retorna o valor 3.

class MinhaClasse():
    def metodo_retorna_3(self):
        return 3

instancia = MinhaClasse()
instancia.metodo_retorna_3()
>> 3
# até aqui, tudo como deveria ser...

Então eu "mocko" o objeto ... (sim, eu "mock",tu "mocka", ele "mocka" - todo mundo que faz teste "mocka").

>>># mas quando chamamos o `mock`
>>>from unittest.mock import MagicMock
>>>instancia.metodo_retorna_3 = MagicMock(return_value=1)  # especifíca o valor a ser retornado.
>>>instancia.metodo_retorna_3()
1

Veja bem, o meu código continua chamando o método da minha instância com o mesmo nome mas agora ele retorna um valor diferente porque assim foi setado pelo objeto Mock.

O objeto Mock, ele não só permite que eu imite um resultado, mas ele aceita qualquer coisa que eu passar para ele como parâmetro.

>>># não importa os argumentos... o método NEM tinha argumento >>>instancia.metodo_retorna_3(2, 3, 4, 5, kye_arg='argumento') >> 1

Nem mesmo um método precisa existir previamente ... embora "mockar" um método inexistente não faça muito sentido pra mim agora, mas para ficar claro que é possível, veja abaixo.

instancia.metodo_inexistente = MagicMock(return_value='existo na instancia')
instancia.metodo_inexistente()
>> 'existo na instancia'

Objetos como o Mock e o MagicMock criam os atributos e os métodos quando os acessamos.

from unittest.mock import MagicMock

É possível limitar este comportamento usando o patch

from unittest.mock import patch

Resumindo, objeto::Mock() aceita qulquer coisa, método ou argumentos se eu não disser o contrário e assume qualquer nome e a forma

veja o seguinte ...

>>> d = dict()
>>> d = dict([('a',1), ('b', 2)])
dict_keys(['a', 'b'])

O objeto::Mock permite tudo e qualquer coisa, mas e se for necessário limitar os atributos e os métodos ? Modificar somente parte do comportamento padrão de um objeto ? autospec=True veja os exemplos.

Mock & Patch

Mock: é um objeto

Patch: é um contexto

Outra maneira de entender Mock e Patch ...

Mock:
Usado para substituir algo que está no mesmo contexto
Pacth:
Usado para substituir algo que foi importado ou criado em outro contexto

melhor entendido através de exemplos.

O Patch

O patch irá interceptar o import e retornar uma instância Mock - uma instância mockada.

O Patch

  • 1 teste por vez (teste unidade/unitário)
  • Testar o que ?
  • Cuidado para não testar códigos de terceiros... do ponto de vista do seu código.