web-dev-qa-db-fra.com

Comment utiliser correctement mock dans python avec unittest setUp

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:

  1. Cela vous semble-t-il correct? :)
  2. Est-ce la bonne façon d'essayer de se moquer d'un objet qui est instancié dans la classe que je teste?
  3. Est-il correct d'appeler le décorateur @patch sur setUp ou cela va-t-il provoquer des effets secondaires étranges?
  4. Existe-t-il de toute façon une simulation pour déclencher l'exception ldap.INVALID_CREDENTIALS sans avoir à importer l'exception dans mon fichier testcase?
  5. Dois-je plutôt utiliser patch.object () et si oui, comment?

Merci.

53
sjmh

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.

56
jooks

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()
10
Danny Staple

Je vais commencer par répondre à vos questions, puis je donnerai un exemple détaillé de la façon dont patch() et setUp() interagissent.

  1. Je ne pense pas que cela semble correct, voir ma réponse à la question # 3 dans cette liste pour plus de détails.
  2. Oui, l'appel réel au correctif semble se moquer de l'objet que vous souhaitez.
  3. Non, vous ne voulez presque jamais utiliser le décorateur @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.
  4. Je ne connais aucun moyen de faire un objet simulé déclencher une exception sans importer cette exception dans votre fichier de cas de test.
  5. Je ne vois aucun besoin de 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)
10
Don Kirkby

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.

4
Kurt Peek

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()
0
Karpad