python mock – een methode patchen zonder de implementatie te belemmeren

Is er een schone manier om een ​​object te patchen zodat je de assert_call*-helpers in je testcase krijgt, zonder de actie daadwerkelijk te verwijderen?

Hoe kan ik bijvoorbeeld de regel @patchwijzigen om de volgende test te laten slagen:

from unittest import TestCase
from mock import patch
class Potato(object):
    def foo(self, n):
        return self.bar(n)
    def bar(self, n):
        return n + 2
class PotatoTest(TestCase):
    @patch.object(Potato, 'foo')
    def test_something(self, mock):
        spud = Potato()
        forty_two = spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

Ik zou dit waarschijnlijk samen kunnen hacken met behulp van side_effect, maar ik hoopte dat er een leukere manier zou zijn die op dezelfde manier werkt voor alle functies, klassenmethoden, statische methoden, ongebonden methoden, enz.


Antwoord 1, autoriteit 100%

Vergelijkbare oplossing als die van jou, maar met wraps:

def test_something(self):
    spud = Potato()
    with patch.object(Potato, 'foo', wraps=spud.foo) as mock:
        forty_two = spud.foo(n=40)
        mock.assert_called_once_with(n=40)
    self.assertEqual(forty_two, 42)

Volgens de documentatie:

wraps: item voor het namaakobject om in te pakken. Als wraps niet Geen is, dan
door de Mock te bellen, wordt de oproep doorgestuurd naar het ingepakte object
(teruggeven van het echte resultaat). Attribuuttoegang op de mock zal terugkeren
een Mock-object dat het corresponderende attribuut van de wrap omwikkelt
object (dus proberen toegang te krijgen tot een attribuut dat niet bestaat zal
een AttributeError opwerpen).


class Potato(object):
    def spam(self, n):
        return self.foo(n=n)
    def foo(self, n):
        return self.bar(n)
    def bar(self, n):
        return n + 2
class PotatoTest(TestCase):
    def test_something(self):
        spud = Potato()
        with patch.object(Potato, 'foo', wraps=spud.foo) as mock:
            forty_two = spud.spam(n=40)
            mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

Antwoord 2, autoriteit 24%

Dit antwoord beantwoordt aan de aanvullende vereiste die wordt genoemd in de premie van gebruiker Quuxplusone:

Het belangrijkste voor mijn use-case is dat het werkt met @patch.mock, dat wil zeggen dat ik geen code hoef in te voegen tussen mijn constructie van de instantie van Potato(spudin dit voorbeeld) en mijn aanroep van spud.foo. Ik heb spudnodig om vanaf het begin te worden gemaakt met een nagemaakte foo-methode, omdat ik geen controle heb over de plaats waar spudis gemaakt.

De hierboven beschreven use case kan zonder al te veel moeite worden bereikt door een decorateur te gebruiken:

import unittest
import unittest.mock  # Python 3
def spy_decorator(method_to_decorate):
    mock = unittest.mock.MagicMock()
    def wrapper(self, *args, **kwargs):
        mock(*args, **kwargs)
        return method_to_decorate(self, *args, **kwargs)
    wrapper.mock = mock
    return wrapper
def spam(n=42):
    spud = Potato()
    return spud.foo(n=n)
class Potato(object):
    def foo(self, n):
        return self.bar(n)
    def bar(self, n):
        return n + 2
class PotatoTest(unittest.TestCase):
    def test_something(self):
        foo = spy_decorator(Potato.foo)
        with unittest.mock.patch.object(Potato, 'foo', foo):
            forty_two = spam(n=40)
        foo.mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)
if __name__ == '__main__':
    unittest.main()

Als de vervangen methode veranderlijke argumenten accepteert die tijdens het testen zijn gewijzigd, wil je misschien een CopyingMock*in plaats van de MagicMockin de spy_decorator.

*Het is een recept uit de docsdie ik op PyPI heb gepubliceerd als copyingmocklib


Antwoord 3, autoriteit 3%

De OP doet dit verzoek:

Ik zou dit waarschijnlijk samen kunnen hacken met side_effect

Voor degenen die het niet erg vinden om side_effectte gebruiken, hier is een oplossing met een paar voordelen:

  • Gebruikt decorateur-syntaxis
  • Past een ongebonden methode op, die volgens mij veelzijdiger is
    • Vereist opname van de instantie in de bewering
class PotatoTest(TestCase):
    @patch.object(Potato, 'foo', side_effect=Potato.foo, autospec=True)
    def test_something(self, mock):
        spud = Potato()
        forty_two = spud.foo(n=40)
        mock.assert_called_once_with(spud, n=40)
        self.assertEqual(forty_two, 42)

Other episodes