Python burlarse de una función de un módulo importado

125

Quiero entender cómo @patchfunciona una función desde un módulo importado.

Aquí es donde estoy hasta ahora.

app / mocking.py:

from app.my_module import get_user_name

def test_method():
  return get_user_name()

if __name__ == "__main__":
  print "Starting Program..."
  test_method()

app / my_module / __ init__.py:

def get_user_name():
  return "Unmocked User"

test / mock-test.py:

import unittest
from app.mocking import test_method 

def mock_get_user():
  return "Mocked This Silly"

@patch('app.my_module.get_user_name')
class MockingTestTestCase(unittest.TestCase):

  def test_mock_stubs(self, mock_method):
    mock_method.return_value = 'Mocked This Silly')
    ret = test_method()
    self.assertEqual(ret, 'Mocked This Silly')

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

Esto no funciona como esperaba. El módulo "parcheado" simplemente devuelve el valor no manipulado de get_user_name. ¿Cómo puedo simular métodos de otros paquetes que estoy importando en un espacio de nombres bajo prueba?

nsfyn55
fuente
1
La pregunta es sobre "burlarse de las mejores prácticas" o si lo que está haciendo tiene sentido o no. Con respecto al primero, diría que use una biblioteca de burla como Mock, que se incluye en python3.3 + as unittest.mock.
Bakuriu
Estoy preguntando si lo estoy haciendo bien. Miré a Mock, pero no veo la manera de resolver este problema en particular. ¿Hay alguna forma de recrear lo que hice arriba en Mock?
nsfyn55

Respuestas:

167

Cuando se utiliza el patchdecorador del unittest.mockpaquete que está no pacheando el espacio de nombres del módulo ha sido importada desde (en este caso app.my_module.get_user_name) que está remendándolo en el espacio de nombres bajo prueba app.mocking.get_user_name.

Para hacer lo anterior, Mockintente algo como lo siguiente:

from mock import patch
from app.mocking import test_method 

class MockingTestTestCase(unittest.TestCase):

    @patch('app.mocking.get_user_name')
    def test_mock_stubs(self, test_patch):
        test_patch.return_value = 'Mocked This Silly'
        ret = test_method()
        self.assertEqual(ret, 'Mocked This Silly')

La documentación de la biblioteca estándar incluye una sección útil que describe esto.

Matti John
fuente
esto llega a mi problema. get_user_nameestá en un módulo diferente al test_method. ¿Hay alguna forma de simular algo en un sub_módulo? Lo arreglé de una manera fea abajo.
nsfyn55
6
No importa que get_user_nameesté en un módulo diferente al test_methodque está importando la función app.mockingen el mismo espacio de nombres.
Matti John
2
¿De dónde vino test_patch, qué es exactamente?
Mike G
2
El decorador de parches pasa test_patch y es el objeto get_user_name simulado (es decir, una instancia de la clase MagicMock). Sería más claro si se llamara así get_user_name_patch.
Matti John
¿Cómo está haciendo referencia a test_method? Dará como resultado un error, NameError: el nombre global 'test_method' no está definido
Aditya
12

Si bien la respuesta de Matti John resuelve su problema (y también me ayudó, ¡gracias!), Sin embargo, sugeriría localizar el reemplazo de la función 'get_user_name' original con la burlada. Esto le permitirá controlar cuándo se reemplaza la función y cuándo no. Además, esto le permitirá realizar varios reemplazos en la misma prueba. Para hacerlo, use la declaración 'con' de una manera bastante similar:

from mock import patch

class MockingTestTestCase(unittest.TestCase):

    def test_mock_stubs(self):
        with patch('app.mocking.get_user_name', return_value = 'Mocked This Silly'):
            ret = test_method()
            self.assertEqual(ret, 'Mocked This Silly')
Tgilgul
fuente
6
Esto es algo irrelevante para la pregunta planteada. Si lo usa patchcomo decorador o administrador de contexto es específico del caso de uso. Por ejemplo, puede usarlo patchcomo decorador para simular un valor para todas las pruebas en una clase xunito pytest, mientras que en otros casos es útil tener el control detallado que brinda el administrador de contexto.
nsfyn55