Wednesday, January 11, 2017

python mock - patching a method without obstructing implementation

Leave a Comment

Is there a clean way to patch an object so that you get the assert_call* helpers in your test case, without actually removing the action?

For example, how can I modify the @patch line to get the following test passing:

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) 

I could probably hack this together using side_effect, but I was hoping there would be a nicer way which works the same way on all of functions, classmethods, staticmethods, unbound methods, etc.

3 Answers

Answers 1

Similar solution with yours, but using 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) 

According to the documentation:

wraps: Item for the mock object to wrap. If wraps is not None then calling the Mock will pass the call through to the wrapped object (returning the real result). Attribute access on the mock will return a Mock object that wraps the corresponding attribute of the wrapped object (so attempting to access an attribute that doesn’t exist will raise an AttributeError).


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) 

Answers 2

It is possible like this:

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

But I am interested in cleaner answers because this looks quite ugly. It is not easily extendible to the many other use cases of patch.

Answers 3

This answer address the additional requirement mentioned in the bounty from user Quuxplusone:

The important thing for my use-case is that it work with @patch.mock, i.e. that it not require me to insert any code in between my constructing of the instance of Potato (spud in this example) and my calling of spud.foo. I need spud to be created with a mocked-out foo method from the get-go, because I do not control the place where spud is created.

The use case described above could be achieved without too much trouble by using a decorator:

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

If the method replaced accepts mutable arguments which are modified under test, you might wish to initialize a CopyingMock in place of the MagicMock inside the spy_decorator.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment