Wie man Mock in Python mit unittest setUp richtig benutzt

71

Bei meinem Versuch, TDD zu lernen, versuche ich, Unit-Tests zu lernen und Mock mit Python zu verwenden. Langsam den Dreh raus, aber unsicher, ob ich das richtig mache. Vorgewarnt: Ich arbeite nicht mit Python 2.4, da die Hersteller-APIs als vorkompilierte 2.4-Pyc-Dateien vorliegen. Daher verwende ich Mock 0.8.0 und unittest (nicht unittest2).

Angesichts dieses Beispielcodes in '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

Jetzt möchte ich in meiner Testfalldatei 'test_myclass.py' das ldap-Objekt verspotten. ldap.initialize gibt das ldap.ldapobject.SimpleLDAPObject zurück, also dachte ich mir, das wäre die Methode, die ich verspotten müsste.

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

Führt mich zu ein paar Fragen:

  1. Sieht das richtig aus :) :)
  2. Ist das der richtige Weg, um ein Objekt zu verspotten, das innerhalb der Klasse, die ich teste, instanziiert wird?
  3. Ist es in Ordnung, den @ Patch-Dekorateur bei setUp anzurufen, oder führt dies zu seltsamen Nebenwirkungen?
  4. Gibt es überhaupt eine Möglichkeit, die Ausnahme ldap.INVALID_CREDENTIALS auszulösen, ohne die Ausnahme in meine Testfalldatei importieren zu müssen?
  5. Sollte ich stattdessen patch.object () verwenden und wenn ja, wie?

Vielen Dank.

sjmh
quelle
1
1-3) Scheint mir in Ordnung zu sein ... 4) import ldapstattdessen und setzen side_effect = ldap.INVALID_CREDENTIALS?
Chris
Sie können immer den gleichen Test machen, aber mit einfacheren Objekten, die Sie selbst gemacht haben ...
Shackra

Antworten:

76

Sie können patch()als Klassendekorateur verwenden, nicht nur als Funktionsdekorateur. Sie können dann die verspottete Funktion wie zuvor übergeben:

@patch('mymodule.SomeClass')
class MyTest(TestCase):

    def test_one(self, MockSomeClass):
        self.assertIs(mymodule.SomeClass, MockSomeClass)

Siehe: Anwenden des gleichen Patches auf jede Testmethode (in der auch Alternativen aufgeführt sind)

Es ist sinnvoller, den Patcher auf setUp auf diese Weise einzurichten, wenn das Patching für alle Testmethoden durchgeführt werden soll.

scherzt
quelle
11
Ich bin gerade auf ein Problem gestoßen, bei dem ich eine TestCase-Klasse auf Klassenebene verspottet habe und davon ausgegangen bin, dass sie beim Aufrufen der setUp()Methode bereits vorhanden ist. DAS IST NICHT DER FALL; Mocks auf Klassenebene werden nicht rechtzeitig für die Verwendung in angewendet setUp(). Ich habe das Problem gelöst, indem ich stattdessen eine Hilfsmethode erstellt habe, die ich in all meinen Tests verwende. Ich bin mir nicht sicher, ob dies der beste Ansatz ist, aber er funktioniert.
Berto
@berto Wenn Sie Ihren Kommentar in einer Antwort erweitern, denke ich, dass es hilfreich sein wird. Es ist eine andere und wahrscheinlich einfachere Lösung als die anderen hier.
KobeJohn
19

Ich beantworte zunächst Ihre Fragen und gebe dann ein detailliertes Beispiel dafür, wie patch()und wie Sie setUp()interagieren.

  1. Ich denke nicht, dass es richtig aussieht. Weitere Informationen finden Sie in meiner Antwort auf Frage 3 in dieser Liste.
  2. Ja, der eigentliche Aufruf zum Patchen sieht so aus, als sollte er das gewünschte Objekt verspotten.
  3. Nein, Sie möchten den @patch()Dekorateur fast nie verwenden setUp(). Sie haben Glück gehabt, denn das Objekt wird in erstellt setUp()und wird während der Testmethode nie erstellt.
  4. Ich kenne keine Möglichkeit, ein Scheinobjekt dazu zu bringen, eine Ausnahme auszulösen, ohne diese Ausnahme in Ihre Testfalldatei zu importieren.
  5. Ich sehe hier keine Notwendigkeit patch.object(). Sie können damit nur Attribute eines Objekts patchen, anstatt das Ziel als Zeichenfolge anzugeben.

Um meine Antwort auf Frage 3 zu erweitern, besteht das Problem darin, dass der patch()Dekorateur nur angewendet wird, während die dekorierte Funktion ausgeführt wird. Sobald Sie setUp()zurückkehren, wird der Patch entfernt. In Ihrem Fall funktioniert das, aber ich wette, es würde jemanden verwirren, der sich diesen Test ansieht. Wenn Sie wirklich nur möchten, dass der Patch während ausgeführt wird setUp(), würde ich vorschlagen, die withAnweisung zu verwenden, um zu verdeutlichen, dass der Patch entfernt wird.

Das folgende Beispiel enthält zwei Testfälle. TestPatchAsDecoratorzeigt, dass beim Dekorieren der Klasse der Patch während der Testmethode angewendet wird, jedoch nicht während setUp(). TestPatchInSetUpzeigt, wie Sie den Patch so anwenden können, dass er sowohl während als auch während setUp()der Testmethode vorhanden ist. Durch das Aufrufen self.addCleanUp()wird sichergestellt, dass der Patch während entfernt wird 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)
Don Kirkby
quelle
Ich denke, Sie möchten einen Hyperlink zu der Antwort Nr. 3 geben , die Sie erwähnt haben, da SO die Antworten nach den Punkten sortiert, die sie erhalten.
Erdin Eray
Ich verstehe, was du meinst, @ErdinEray, aber ich habe tatsächlich über meine Antwort auf Frage 3 des OP gesprochen.
Don Kirkby
Diese Technik gefällt mir sehr gut, da Sie damit eine Standardkonfiguration für ein Modell auf Klassenebene erstellen können, das für die meisten Tests geeignet ist. Tests, bei denen sich das Modell auf unterschiedliche Weise verhalten muss, können dies überschreiben. Wirklich ganz nett.
Adam Parkin
13

Wenn Sie viele Patches anwenden müssen und diese auch auf Dinge angewendet werden sollen, die mit den setUp-Methoden initialisiert wurden, versuchen Sie Folgendes:

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()
Danny Staple
quelle
5
betrachten die Verwendung patch.stop_all()in tearDown().
Michele d'Amico
1
Ich habe es versucht - anscheinend müssen auch die Applied_Patches gestartet werden. Betrachten Sie eine Zeile wie:for patch in self.applied_patches: patch.start()
F1Rumors
Es ist stopallnicht stop_all.
OrangeDog
3
Fairerweise würde ich jetzt die Methode "self.addCleanup (patch)" verwenden. Zeit, diese Antwort zu aktualisieren.
Danny Staple
self.addCleanup(patch.stopall)
Jerther
11

Ich möchte auf eine Variation der akzeptierten Antwort hinweisen, in der ein newArgument an den patch()Dekorateur weitergegeben wird:

from unittest.mock import patch, Mock

MockSomeClass = Mock()

@patch('mymodule.SomeClass', new=MockSomeClass)
class MyTest(TestCase):
    def test_one(self):
        # Do your test here

Beachten Sie, dass in diesem Fall nicht mehr MockSomeClasszu jeder Testmethode das zweite Argument hinzugefügt werden muss, wodurch viel Code-Wiederholung eingespart werden kann.

Eine Erklärung hierzu finden Sie unter https://docs.python.org/3/library/unittest.mock.html#patch :

Wenn patch()es als Dekorateur verwendet wird und neu weggelassen wird, wird das erstellte Modell als zusätzliches Argument an die dekorierte Funktion übergeben.

Die Antworten lassen vor allem neue aus , aber es kann bequem sein, sie aufzunehmen.

Kurt Peek
quelle
1
Danke dafür! Dies ist sehr hilfreich, insbesondere wenn es viele verspottete Funktionsklassen gibt, für die kein spezieller Rückgabewert oder ähnliches erforderlich ist. Hält die Funktionsdefinitionen der Testfälle sauberer.
Asmo Soinio
Danke, das hat mir geholfen, meinen Code sauberer und trockener zu machen. Ich muss nicht zu jeder Methode Anmerkungen machen, da ich einige Scheinklassen hatte.
Sukhinderpal Mann
0

Sie können eine gepatchte innere Funktion erstellen und von dort aus aufrufen setUp.

Wenn Ihre ursprüngliche setUpFunktion ist:

def setUp(self):
    some_work()

Dann können Sie es patchen, indem Sie es ändern in:

def setUp(self):
    @patch(...)
    def mocked_func():
        some_work()

    mocked_func()
Karpad
quelle
Warum also nicht einfach patchals Kontextmanager verwenden? with patch(...):
Fund Monica Klage