Curiosity

Musings on observations.

Mocking Python With Kung Fu Panda

I frequently use the Mock library for unit testing in python. I needed a quick reference for my favorite functionality and couldn’t find one. I decided to make a lighthearted attempt at writing one while watching Kung Fu Panda on the telly. I hope others find it useful.

Before I start, here is the list of my favorites:

  1. Mock classes
  2. Mock class methods
  3. Mock instances
  4. Mock instance methods
  5. Configurable return values
  6. Restricted API for mock objects
  7. MagicMock
  8. Mock multiple classes
  9. Verifying calls
  10. Sentinels

Let’s start with a couple of classes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from mock import Mock, patch
  
class KungFuPanda:

  def __init_(self):
      self.secret_ingredient = "passion"
      
  def foo(self):
      super_long_sleep()
      return "soup"
  
  def bar(self, instructor):
      if instructor.message == "punch":
          return "hug"
      else:
          return "whatever"
          
  def hello(self):
      return "ahoy!"


class Instructor:
  
  tribe = "know_it_all"
  
  def __init__(self, name):
      self.name = name
  
  def message(self, message):
      self.message = speak_and_get_message_from_the_entire_community()
  
  @classmethod    
  def get_tribe(cls):
      return cls.tribe

Mock classes

A KungFuPanda performs two actions – foo and bar. We want to test the output of the bar action when it gets a message from an instructor. However, the instructor needs to speak to the entire community before coming up with a message to test the KungFuPanda. We decide to mock the instructor class and quickly test the panda.

1
2
3
4
5
6
7
8
9
10
11
In [2]: kf = KungFuPanda()

In [3]: mock_instructor = Mock(message = "punch")

In [4]:  kf.bar(mock_instructor)
Out[4]: 'hug'

In [5]: mock_instructor.message = "kill"

In [6]: kf.bar(mock_instructor)
Out[6]: 'whatever'

Mock class methods

The instructors comes from the know_it_all tribe. What if we wanted to have fun at their expense? Mocking class methods does the trick.

1
2
3
4
@patch.object(Instructor,'get_tribe')
def tribe_fun(mock_get_tribe):
  mock_get_tribe.return_value = "simpleton"
  return Instructor.get_tribe()

Let the fun begin!

1
2
3
4
5
In [8]: Instructor.get_tribe()
Out[8]: 'know_it_all'

In [9]: tribe_fun()
Out[9]: 'simpleton'

Mock instances

I love the way the KungFuPandas say ahoy! when greeted. But let’s get a non-pirate greeting from them by creating mock versions.

1
2
3
4
5
@patch(__name__ + '.KungFuPanda')
def hello_panda(MockKungFuPanda):
  instance = MockKungFuPanda.return_value  
  instance.hello.return_value = "hi there"
  return KungFuPanda().hello()

Let’s say hello.

1
2
3
4
5
In [11]: KungFuPanda().hello()
Out[11]: 'ahoy!'

In [12]: hello_panda()
Out[12]: 'hi there'

Mock instance methods

Let’s mock the hello instance method for fun.

1
2
3
4
5
6
In [13]: kf_mocking_panda = KungFuPanda()

In [14]: kf_mocking_panda.hello = Mock(return_value = "mock you!")

In [15]: kf_mocking_panda.hello()
Out[15]: 'mock you!'

Configurable return values

Suppose a panda wanted to fake it’s behavior after receiving instruction, it could use side_effects to mock the behavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [5]: def good_behavior_side_effect(args):
   ...:    return good_behavior[args]
   ...:

In [6]: good_behavior = {"punch" : "punching!", "punch_some_more" : "punching again!"}

In [7]: obedient_pandas = Mock()

In [8]: obedient_pandas.bar.side_effect = good_behavior_side_effect

In [9]: obedient_pandas.bar("punch")
Out[9]: 'punching!'

In [10]: obedient_pandas.bar("punch_some_more")
Out[10]: 'punching again!'

In [11]: obedient_pandas.bar("punch_again")
KeyError: 'punch_again'

The obedient_panda can also mock a sequence of behaviors when asked to foo.

1
2
3
4
5
6
7
In [12]: obedient_pandas.hello.side_effect = ["hello","hello again!"]

In [13]: obedient_pandas.hello()
Out[13]: 'hello'

In [14]: obedient_pandas.hello()
Out[14]: 'hello again!'

Restricted API for mock objects

The Instructors find it acceptable to be mocked (only for testing!) but don’t like extending their API even during testing. For example, giving them a name as shown below is unacceptable

1
2
3
4
5
6
7
8
9
In [15]: stupid_instructor = Mock()

In [16]: stupid_instructor.name.return_value = "dimwit"

In [17]: stupid_instructor.name
Out[17]: <Mock name='mock.name' id='4350823824'>

In [18]: stupid_instructor.name()
Out[18]: 'dimwit'

Ok, let’s try to be respectful of the Instructor API by using auto_spec.

1
2
3
4
5
6
7
In [24]: from mock import create_autospec

In [28]: no_named_instructor.message("hello")
Out[28]: <MagicMock name='mock.message()' id='4350658000'>

In [29]: no_named_instructor.name.return_value = "dimwit"
AttributeError: Mock object has no attribute 'name'

Magic Mock

MagicMock provides default implementations for several magic methods.

1
2
3
4
5
6
In [30]: from mock import MagicMock

In [31]: some_mock = MagicMock()

In [34]: len(some_mock), int(some_mock), float(some_mock), list(some_mock)
Out[34]: (0, 1, 1.0, [])

Mock multiple classes

Mocking multiple classes is straightforward to do but I often get the nesting order wrong for the patch decorators. Let’s mock both KungFuPanda and Instructor classes.

1
2
3
4
5
6
7
8
9
@patch(__name__ + '.KungFuPanda')
@patch(__name__ + '.Instructor')
def test_mock_multiple_classes(MockInstructor,MockPanda):
    MockPanda.name = "peace-loving kung fu panda"
    MockInstructor.name = "The Great"
    print KungFuPanda.name, " is coached by ", Instructor.name

In [40]: test_mock_multiple_classes()
peace-loving kung fu panda is coached by  The Great

Verifying calls

It’s easy to know all the calls made using mock. I have shown an example using the call_args list option that I use frequently. For more options, please see the documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
In [77]:  knowing_panda = KungFuPanda()

In [78]: knowing_panda.bar = Mock()

In [79]: knowing_panda.bar.return_value = "fake_bar"

In [80]: knowing_panda.bar("message")
Out[80]: 'fake_bar'

In [81]: knowing_panda.bar("message_2")
Out[81]: 'fake_bar'

In [82]: knowing_panda.bar("message_3")
Out[82]: 'fake_bar'

In [83]: knowing_panda.bar.call_args_list
Out[83]: [call('message'), call('message_2'), call('message_3')]

In [84]: knowing_panda.bar.call_args
Out[84]: call('message_3')

Sentinels

Finally, sentinels can be used to return uniquely named objects.

1
2
3
4
5
6
7
8
9
10
In [45]: from mock import sentinel

In [46]: instructor = Instructor("The Instructor")

In [47]: instructor.message = Mock()

In [48]: instructor.message.return_value = sentinel.fake_message

In [49]: instructor.message()
Out[49]: sentinel.fake_message

Happy mocking!

Comments