mock - Cómo utilizar correctamente el simulacro en Python con la configuración de unittest
pytest mock (4)
Comenzaré por responder a sus preguntas, y luego daré un ejemplo detallado de cómo interactúan patch()
y setUp()
.
- No creo que se vea bien, vea mi respuesta a la pregunta # 3 en esta lista para más detalles.
- Sí, la llamada real al parche parece que debería burlarse del objeto que desea.
- No, casi nunca quieres usar el decorador
setUp()
ensetUp()
.setUp()
suerte, porque el objeto se crea ensetUp()
y nunca se crea durante el método de prueba. - No conozco ninguna forma de hacer que un objeto simulado genere una excepción sin importar esa excepción en su archivo de caso de prueba.
- No veo ninguna necesidad de
patch.object()
aquí. Simplemente le permite parchear los atributos de un objeto en lugar de especificar el objetivo como una cadena.
Para ampliar mi respuesta a la pregunta # 3, el problema es que el decorador patch()
solo se aplica mientras se ejecuta la función decorada. Tan pronto como setUp()
vuelve, el parche se elimina. En su caso, eso funciona, pero apuesto a que confundiría a alguien al ver esta prueba. Si realmente solo desea que el parche ocurra durante setUp()
, sugeriría usar la instrucción with
para que sea obvio que el parche se eliminará.
El siguiente ejemplo tiene dos casos de prueba. TestPatchAsDecorator
muestra que decorar la clase aplicará el parche durante el método de prueba, pero no durante setUp()
. TestPatchInSetUp
muestra cómo se puede aplicar el parche para que esté en su lugar durante la setUp()
y el método de prueba. Al llamar a self.addCleanUp()
se asegura de que el parche se quitará durante tearDown()
.
import unittest
from mock import patch
@patch(''__builtin__.sum'', return_value=99)
class TestPatchAsDecorator(unittest.TestCase):
def setUp(self):
s = sum([1, 2, 3])
self.assertEqual(6, s)
def test_sum(self, mock_sum):
s1 = sum([1, 2, 3])
mock_sum.return_value = 42
s2 = sum([1, 2, 3])
self.assertEqual(99, s1)
self.assertEqual(42, s2)
class TestPatchInSetUp(unittest.TestCase):
def setUp(self):
patcher = patch(''__builtin__.sum'', return_value=99)
self.mock_sum = patcher.start()
self.addCleanup(patcher.stop)
s = sum([1, 2, 3])
self.assertEqual(99, s)
def test_sum(self):
s1 = sum([1, 2, 3])
self.mock_sum.return_value = 42
s2 = sum([1, 2, 3])
self.assertEqual(99, s1)
self.assertEqual(42, s2)
En mi intento de aprender TDD, intentar aprender pruebas de unidad y usar simulacro con python. Lentamente aprendiendo, pero no estoy seguro de si estoy haciendo esto correctamente. Prevenido: estoy trabajando con python 2.4 porque las API del proveedor vienen como archivos de 2.4 pyc precompilados, así que estoy usando simulacro 0.8.0 y unittest (no unittest2)
Dado este código de ejemplo en ''mymodule.py''
import ldap
class MyCustomException(Exception):
pass
class MyClass:
def __init__(self, server, user, passwd):
self.ldap = ldap.initialize(server)
self.user = user
self.passwd = passwd
def connect(self):
try:
self.ldap.simple_bind_s(self.user, self.passwd)
except ldap.INVALID_CREDENTIALS:
# do some stuff
raise MyCustomException
Ahora en mi archivo de caso de prueba ''test_myclass.py'', quiero burlarme del objeto ldap. ldap.initialize devuelve el ldap.ldapobject.SimpleLDAPObject, así que pensé que ese sería el método que tendría que burlarme.
import unittest
from ldap import INVALID_CREDENTIALS
from mock import patch, MagicMock
from mymodule import MyClass
class LDAPConnTests(unittest.TestCase):
@patch(''ldap.initialize'')
def setUp(self, mock_obj):
self.ldapserver = MyClass(''myserver'',''myuser'',''mypass'')
self.mocked_inst = mock_obj.return_value
def testRaisesMyCustomException(self):
self.mocked_inst.simple_bind_s = MagicMock()
# set our side effect to the ldap exception to raise
self.mocked_inst.simple_bind_s.side_effect = INVALID_CREDENTIALS
self.assertRaises(mymodule.MyCustomException, self.ldapserver.connect)
def testMyNextTestCase(self):
# blah blah
Me lleva a un par de preguntas:
- ¿Se ve bien? :)
- ¿Es esa la forma correcta de intentar simular un objeto que se crea una instancia dentro de la clase que estoy probando?
- ¿Está bien llamar al decorador @patch en la configuración o esto va a causar efectos secundarios extraños?
- ¿Hay alguna forma de simular que se genere la excepción ldap.INVALID_CREDENTIALS sin tener que importar la excepción a mi archivo de prueba?
- ¿Debería estar usando patch.object () en su lugar y si es así, cómo?
Gracias.
Me gustaría señalar una variación de la respuesta aceptada en la que se pasa un new
argumento al decorador patch()
:
from unittest.mock import patch, Mock
MockSomeClass = Mock()
@patch(''mymodule.SomeClass'', new=MockSomeClass)
class MyTest(TestCase):
def test_one(self):
# Do your test here
Tenga en cuenta que en este caso, ya no es necesario agregar el segundo argumento, MockSomeClass
, a cada método de prueba, que puede ahorrar mucha repetición de código.
Puede encontrar una explicación de esto en https://docs.python.org/3/library/unittest.mock.html#patch :
Si se usa
patch()
como decorador y se omite el nuevo , el simulacro creado se pasa como un argumento adicional a la función decorada.
Las respuestas sobre todo omiten lo nuevo , pero puede ser conveniente incluirlo.
Puede usar patch()
como decorador de clase, no solo como decorador de funciones. A continuación, puede pasar en la función simulada como antes:
@patch(''mymodule.SomeClass'')
class MyTest(TestCase):
def test_one(self, MockSomeClass):
self.assertIs(mymodule.SomeClass, MockSomeClass)
Ver: 26.5.3.4. Aplicar el mismo parche a cada método de prueba (que también enumera alternativas)
Tiene más sentido configurar el parche de esta manera en la configuración si desea que se realicen los parches para todos los métodos de prueba.
Si tiene muchos parches para aplicar y desea que se apliquen a las cosas inicializadas en los métodos de configuración, intente esto:
def setUp(self):
self.patches = {
"sut.BaseTestRunner._acquire_slot": mock.Mock(),
"sut.GetResource": mock.Mock(spec=GetResource),
"sut.models": mock.Mock(spec=models),
"sut.DbApi": make_db_api_mock()
}
self.applied_patches = [mock.patch(patch, data) for patch, data in self.patches.items()]
[patch.apply for patch in self.applied_patches]
.
. rest of setup
.
def tearDown(self):
patch.stopall()