{lang: 'pt-BR'}

Se uma boa cobertura de testes é importante em linguagens fortemente tipadas, ela se torna indispensável em linguagens dinâmicas, como Python. A falta de verificação de tipos no processo de build torna a aplicação muito mais suscetível a erros. Testar com Python não é uma tarefa muito difícil. O Python já vem com uma suite de testes built-in e até primitivas de testes na própria linguagem.

A boa prática dos testes unitários diz que devemos isolar um método de qualquer dependência externa, a fim de testarmos um módulo de cada vez. Por isso, não são raras as vezes que precisamos simular o resultado de uma função (mock). Existem inúmeras maneiras de fazer isso usando frameworks de mock, mas a maneira mais simples é redefinirmos a função.

Suponhamos que temos o seguinte cenário:

Mock - Python

Queremos testar a função func_b(), que está no módulo b.py, que depende da função func_a(), que está no módulo a.py.

Em nosso teste teríamos:

import unittest
from b import func_b

class Test(unittest.TestCase):
    def test_func_b(self):
        val = func_b()
        assert val

Para isto ser realmente um teste unitário, temos que isolar a chamada da função func_a() de dentro da função func_b(). Algo como:

import unittest
from b import func_b
import a

class Test(unittest.TestCase):
    def test_func_b(self):
        def mock_func_a():
            return 10
        old_func_a = a.func_a
        a.func_a = mock_func_a
        val = func_b()
        assert val
        a.func_a = old_func_a

Nosso código cria uma função mock_func_a() que simula o comportamento da função func_a() para ser chamada pela função func_b(). Para não perdermos o antigo valor da função func_a() guardamos uma referência para ela numa variável chamada old_func_a e resgatamos o valor original ao final do teste.

Neste exemplo estamos mudando a referência da função func_a() dentro do módulo a.py (linha 10). Acontece que isso NÃO funciona. A razão é que o módulo b.py guarda uma referência para o endereço da função func_a() e não para o nome dela. Quando fazemos “from a import func_a” no módulo b.py estamos guardando o endereço de memória onde está a função func_a() e, ao redefinir o valor da função, não estamos redefinindo o endereço de memória original e, portanto, nada muda no módulo b.py. A função func_b() continuará chamando a versão original da função func_a().

O que precisamos fazer é redefinir o valor da função func_a() dentro do módulo b.py. Precisamos dizer ao módulo b.py que aquela função que ele importou não é mais para ser usada, e sim uma outra. Toda vez que fazemos um import no python, criamos também uma referência local naquele módulo para aquilo que estamos importando. Assim sendo, podemos trocar esta referência local ao módulo b.py.

Nosso código de teste ficará assim:

import unittest
import b

class Test(unittest.TestCase):
    def test_func_b(self):
        def mock_func_a():
            return 10
        old_func_a = b.func_a
        b.func_a = mock_func_a
        val = b.func_b()
        assert val
        b.func_a = old_func_a

Repare agora, que na linha 10 estamos redefinindo a função func_a() no módulo b importado na linha 3. A nova função func_a só muda para o módulo b.py. Todo o restante do código continua enxergando a versão original. Desta forma, temos que a função func_b irá chamar a nova função mock_func_a, que dentro do módulo b.py continua se chamando func_a.

Esse problema não existiria se a função func_a() e a função func_b() fossem criadas no mesmo arquivo, já que na hora da redefinição o módulo onde a função é usada é o mesmo onde a função é definida. Isto também não acontece se estivermos usando classes, já que uma classe dentro de um arquivo é entendindo como um módulo dentro de outro. Não é à toa, que ao importarmos uma classe fazemos “from nome_arquivo import nome_classe“.

Espero que tenha sido claro. Mas se não fui, não se acanhe de fazer uma pergunta nos comentários. :)

{lang: 'pt-BR'}