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.