Dans ma tentative d'apprendre TDD, d'essayer d'apprendre les tests unitaires et d'utiliser la simulation avec python. J'y parviens lentement, mais je ne sais pas si je fais cela correctement. Prévenu: je bloque en utilisant python 2.4 parce que les API du fournisseur sont fournies sous forme de fichiers pyc 2.4 précompilés, donc j'utilise mock 0.8.0 et unittest (not unittest2)
Étant donné cet exemple de code dans '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
Maintenant, dans mon fichier de cas de test 'test_myclass.py', je veux me moquer de l'objet LDAP. ldap.initialize renvoie le ldap.ldapobject.SimpleLDAPObject, donc j'ai pensé que ce serait la méthode que je devrais utiliser.
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 conduit à quelques questions:
Merci.
Vous pouvez utiliser patch()
comme décorateur de classe, pas seulement comme décorateur de fonction. Vous pouvez ensuite passer la fonction simulée comme précédemment:
@patch('mymodule.SomeClass')
class MyTest(TestCase):
def test_one(self, MockSomeClass):
self.assertIs(mymodule.SomeClass, MockSomeClass)
Voir: 26.5.3.4. Application du même patch à chaque méthode de test (qui répertorie également les alternatives)
Il est plus logique de configurer le correctif de cette façon sur setUp si vous souhaitez que le correctif soit effectué pour toutes les méthodes de test.
Si vous avez de nombreux correctifs à appliquer et que vous souhaitez qu'ils s'appliquent également aux choses initialisées dans les méthodes de configuration, essayez ceci:
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()
Je vais commencer par répondre à vos questions, puis je donnerai un exemple détaillé de la façon dont patch()
et setUp()
interagissent.
@patch()
sur setUp()
. Vous avez eu de la chance, car l'objet est créé dans setUp()
et n'est jamais créé pendant la méthode de test.patch.object()
ici. Il vous permet simplement de patcher les attributs d'un objet au lieu de spécifier la cible sous forme de chaîne.Pour développer ma réponse à la question # 3, le problème est que le décorateur patch()
ne s'applique que lorsque la fonction décorée est en cours d'exécution. Dès que setUp()
revient, le patch est supprimé. Dans votre cas, cela fonctionne, mais je parie que cela dérouterait quelqu'un en regardant ce test. Si vous ne voulez vraiment que le patch se produise pendant setUp()
, je suggère d'utiliser l'instruction with
pour rendre évident que le patch va être supprimé.
L'exemple suivant comporte deux cas de test. TestPatchAsDecorator
montre que décorer la classe appliquera le correctif pendant la méthode de test, mais pas pendant setUp()
. TestPatchInSetUp
montre comment appliquer le correctif pour qu'il soit en place pendant setUp()
et la méthode de test. L'appel de self.addCleanUp()
garantit que le patch sera supprimé pendant 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)
Je voudrais souligner une variante de la réponse acceptée dans laquelle un argument new
est passé au décorateur 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
Notez que dans ce cas, il n'est plus nécessaire d'ajouter le deuxième argument, MockSomeClass
, à chaque méthode de test, ce qui peut économiser beaucoup de répétition de code.
Une explication de cela peut être trouvée sur https://docs.python.org/3/library/unittest.mock.html#patch :
Si
patch()
est utilisé comme décorateur et new est omis, la maquette créée est passée comme argument supplémentaire à la fonction décorée .
Les réponses omettent surtout new , mais il peut être pratique de l'inclure.
Vous pouvez créer une fonction interne corrigée et l'appeler à partir de setUp
.
Si votre fonction setUp
d'origine est:
def setUp(self):
some_work()
Ensuite, vous pouvez le patcher en le changeant en:
def setUp(self):
@patch(...)
def mocked_func():
some_work()
mocked_func()