Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Mockando o que tá meio mockado

Um mock especial para Python unittest

To Mock or not to mock?

Ah, um problema

E estava eu preocupado com um teste que deveria escrever para uma aplicação Django. Era muita coisa pra preparar o contexto da função a ser chamada. E deixei quieto, no fim de semana resolvo isso, só isso.

Com a cabeça mais fria, liguei-me que eu estava querendo retestar o que estava testado, e, portando, a pior parte de tudo poderia ser resolvida com um mock.

Coisa simples, pois a aplicação está bem estruturada, funções muito específicas, que são chamadas por outras funções específicas, enfim.

O patch tava lá, com o nome certinho da função e o caminho.

Simulando

Como se trata de código da empresa em que trabalho, aqui vai um simulado, que está:


.
├── __init__.py
├── mod1
│   ├── __init__.py
│   ├── services.py
│   └── tests
│       ├── __init__.py
│       └── test_mod1.py
└── mod2
    ├── __init__.py
    ├── services.py
    └── tests
        ├── __init__.py
        └── test_mod2.py

Isso é o mod2/services.py:


def mod2_function():
    return 'This is REAL mod2!'

E isso é o mod1/services.py:


from mod2.services import mod2_function


def mod1_function():
    mod2 = mod2_function()

return 'From _mod1_ I get this from _mod2_: [{}]'.format(mod2)

O detalhe é que mod1 usa mod2.

O teste, intencionalmente falho, para mod1/tests/test_mod1.py:


from unittest import TestCase

from ..services import mod1_function


class TestMod1(TestCase):
    def test_mod1(self):
        expected = 'I don\'t remember'
        result = mod1_function()

self.assertEqual(result, expected)

E o teste para mod2/testes/test_mod2.py:


from unittest import TestCase

from ..services import mod2_function


class TestMod2(TestCase):
    def test_mod2(self):
        expected = 'This is REAL mod2!'
        mod2_result = mod2_function()

self.assertEqual(mod2_result, expected)

Rodando os testes:


paulohrpinheiro@phrp:~/D/p/c/mocking|master✓
➤ python -munittest -v
test_mod1 (mod1.tests.test_mod1.TestMod1) ... FAIL
test_mod2 (mod2.tests.test_mod2.TestMod2) ... ok

======================================================================
FAIL: test_mod1 (mod1.tests.test_mod1.TestMod1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/paulohrpinheiro/Dropbox/projetos/coisas/mocking/mod1/tests/test_mod1.py", line 11, in     test_mod1
    self.assertEqual(result, expected)
AssertionError: 'From _mod1_ I get this from _mod2_: [This is REAL mod2!]' != "I don't remember"
- From _mod1_ I get this from _mod2_: [This is REAL mod2!]
+ I don't remember

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

Deu erro, como esperado. Eu fiz isso para ressaltar o fato de mod1 usar mod2. Essa era a minha situação real. Agora, para arrumar o teste vou mockar um valor:


➤ git diff
diff --git a/mocking/mod1/tests/test_mod1.py b/mocking/mod1/tests/test_mod1.py
index 7ff23c8..a772241 100644
--- a/mocking/mod1/tests/test_mod1.py
+++ b/mocking/mod1/tests/test_mod1.py
@@ -1,11 +1,14 @@
 from unittest import TestCase
+from unittest.mock import patch

 from ..services import mod1_function


 class TestMod1(TestCase):
-    def test_mod1(self):
-        expected = 'I don\'t remember'
+    @patch('mod2.services.mod2_function')
+    def test_mod1(self, mock_mod2):
+        mock_mod2.return_value = 'MOCKED!'
+        expected = 'From _mod1_ I get this from _mod2_: [MOCKED!]'
         result = mod1_function()

         self.assertEqual(result, expected)

Rodando o teste alterado:


➤ python -munittest -v
test_mod1 (mod1.tests.test_mod1.TestMod1) ... FAIL
test_mod2 (mod2.tests.test_mod2.TestMod2) ... ok

======================================================================
FAIL: test_mod1 (mod1.tests.test_mod1.TestMod1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/paulohrpinheiro/.pyenv/versions/3.6.4/lib/python3.6/unittest/mock.py", line 1179, in patched
    return func(*args, **keywargs)
  File "/home/paulohrpinheiro/Dropbox/projetos/coisas/mocking/mod1/tests/test_mod1.py", line 13, in test_mod1
    self.assertEqual(result, expected)
AssertionError: 'From _mod1_ I get this from _mod2_: [This is REAL mod2!]' != 'From _mod1_ I get this from _mod2_: [MOCKED!]'
- From _mod1_ I get this from _mod2_: [This is REAL mod2!]
?                                      ^^^^^^^^^ ^^^^^^^
+ From _mod1_ I get this from _mod2_: [MOCKED!]
?                                      ^^^^ ^

Lendo e relendo alguns textos, encontrei esse, muito claro a respeito de meu problema:

Patching tip using mocks in python unit tests, do Steve's Blog

Que me deu a luz para essa alteração:


➤ git diff
diff --git a/mocking/mod1/tests/test_mod1.py b/mocking/mod1/tests/test_mod1.py
index 8f66098..2b05c63 100644
--- a/mocking/mod1/tests/test_mod1.py
+++ b/mocking/mod1/tests/test_mod1.py
@@ -5,7 +5,7 @@ from ..services import mod1_function


 class TestMod1(TestCase):
-    @patch('mod2.services.mod2_function', return_value='MOCKED!')
+    @patch('mod1.services.mod2_function', return_value='MOCKED!')
     def test_mod1(self, mock_mod2):
         expected = 'From _mod1_ I get this from _mod2_: [MOCKED!]'
         result = mod1_function()

Finalmente:


➤ python -munittest -v
test_mod1 (mod1.tests.test_mod1.TestMod1) ... ok
test_mod2 (mod2.tests.test_mod2.TestMod2) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

A razão disso

Quando, em mod1/services.py realizo o import, estou adicionando ao namespace local a função mod2_function. Por isso que o caminho para o patch deve ser mod1.services.mod2_funcion, e não o caminho de onde a função foi escrita.