Mocking in unittests in Python

Example - Using mock

Imports

In [35]:
import sys
import unittest
# Python compatibility
if sys.version_info < (3,3):
    import mock
else:
    import unittest.mock as mock

The mock object

Create a mock object:

In [8]:
m = mock.Mock()

Show the attributes of the object:

In [9]:
dir(m)
Out[9]:
['assert_any_call',
 'assert_called_once_with',
 'assert_called_with',
 'assert_has_calls',
 'assert_not_called',
 'attach_mock',
 'call_args',
 'call_args_list',
 'call_count',
 'called',
 'configure_mock',
 'method_calls',
 'mock_add_spec',
 'mock_calls',
 'reset_mock',
 'return_value',
 'side_effect']

Print a fake attribute. It doesn't exist, but will be shown.

In [12]:
m.fake_attribute
Out[12]:
<Mock name='mock.fake_attribute' id='2582717621248'>

Again show the object. This time the fake_attribute will be shown too.

In [13]:
dir(m)
Out[13]:
['assert_any_call',
 'assert_called_once_with',
 'assert_called_with',
 'assert_has_calls',
 'assert_not_called',
 'attach_mock',
 'call_args',
 'call_args_list',
 'call_count',
 'called',
 'configure_mock',
 'fake_attribute',
 'method_calls',
 'mock_add_spec',
 'mock_calls',
 'reset_mock',
 'return_value',
 'side_effect']

Set a return value for the newly introduced attribute and retrieve it.

In [14]:
m.fake_attribute.return_value = "Fake return value"
m.fake_attribute()
Out[14]:
'Fake return value'

Create another attribute, but this time assign a (fake) function to its return_value.

In [108]:
def print_fake_value():
    print("Fake function is called!")

m.another_attribute.return_value = print_fake_value
m.another_attribute()
Out[108]:
<Mock name='mock.another_attribute.print_fake_value()' id='2582735302608'>

Same exercise with a function with an argument:

In [21]:
def print_fake_value_with_arg(argument):
    print("Fake argument %s" % argument)

m.the_third_attribute.return_value = print_fake_value_with_arg
m.the_third_attribute('Print me')
Out[21]:
<function __main__.print_fake_value_with_arg>

You can also create a custom exception by using the side_effect. It can be an exception, callable or an iterable.

In [22]:
m.some_function.side_effect = ValueError("Super error")
m.some_function()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-22-197c49fcbb5e> in <module>()
      1 m.some_function.side_effect = ValueError("Super error")
----> 2 m.some_function()

C:\Users\J-J van Waterschoot\Anaconda3\lib\unittest\mock.py in __call__(_mock_self, *args, **kwargs)
    915         # in the signature
    916         _mock_self._mock_check_sig(*args, **kwargs)
--> 917         return _mock_self._mock_call(*args, **kwargs)
    918 
    919 

C:\Users\J-J van Waterschoot\Anaconda3\lib\unittest\mock.py in _mock_call(_mock_self, *args, **kwargs)
    971         if effect is not None:
    972             if _is_exception(effect):
--> 973                 raise effect
    974 
    975             if not _callable(effect):

ValueError: Super error

To make it an iterable, the following can be used. By calling the mock several times, it will return the values until the limit of the range is reached.

In [23]:
m.some_iteration_thing.side_effect = range(2)
m.some_iteration_thing()
Out[23]:
0
In [24]:
m.some_iteration_thing()
Out[24]:
1
In [25]:
m.some_iteration_thing()
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-25-218e10d9f5de> in <module>()
----> 1 m.some_iteration_thing()

C:\Users\J-J van Waterschoot\Anaconda3\lib\unittest\mock.py in __call__(_mock_self, *args, **kwargs)
    915         # in the signature
    916         _mock_self._mock_check_sig(*args, **kwargs)
--> 917         return _mock_self._mock_call(*args, **kwargs)
    918 
    919 

C:\Users\J-J van Waterschoot\Anaconda3\lib\unittest\mock.py in _mock_call(_mock_self, *args, **kwargs)
    974 
    975             if not _callable(effect):
--> 976                 result = next(effect)
    977                 if _is_exception(result):
    978                     raise result

StopIteration: 

Finally you can also pass a callable to the side_effect, by doing the following:

In [27]:
def side_function():
    print('This is a side function!')
    
m.some_simple_function.side_effect = side_function()
m.some_simple_function()
This is a side function!
Out[27]:
<Mock name='mock.some_simple_function()' id='2582717767408'>
In [28]:
def side_function_with_arg(argument):
    print('This is a side function with argument: %s' % argument)
    
m.some_simple_function_with_arg.side_effect = side_function_with_arg
m.some_simple_function_with_arg('No argument!')
This is a side function with argument: No argument!

An important function of the side_effect is that you can pass it a class, which can be helpful if you are testing code and verify the behaviour of the class

In [29]:
class Car(object):
    def __init__(self, name):
        self._name = name
    def print_name(self):
        print("Name: %s" % self._name)
        
m.a_car_attribute.side_effect = Car
car = m.a_car_attribute.side_effect('My red car')
car
Out[29]:
<__main__.Car at 0x25955fdd6a0>
In [30]:
car.print_name()
Name: My red car

Testing

Castle

Lets define the castle class:

In [99]:
class Castle(object):
    def __init__(self, name):
        self.name = name
        self.boss = 'Bowser'
        self.world = 'Grass Land'

    def access(self, character):
        if character.powerup == 'Super Mushroom':
            return True
        else:
            return False
    
    def get_boss(self):
        return self.boss

    def get_world(self):
        return self.world

We will also define a character class:

In [100]:
class Character(object):
    def __init__(self, name):
        self.name = name
        self.powerup = ''

    def powerup(self, powerup):
        self.powerup = powerup

    def get_powerup(self):
        return self.powerup

Finally we will define a testclass to test the functionality of the classes.

In [101]:
class CharacterTestClass(unittest.TestCase):
    """ Defines the tests for the Character class """
    def setUp(self):
        """ Set the castle for the test cases """
        self.castle = Castle('Bowsers Castle')
        
    def test_mock_access_denied(self):
        """ Access denied for star powerup """
        mock_character = mock.Mock(powerup = 'Starman')
        self.assertFalse(self.castle.access(mock_character))
   
    def test_mock_access_granted(self):
        """ Access granted for mushroom powerup """
        mock_character = mock.Mock(powerup = 'Super Mushroom')
        self.assertTrue(self.castle.access(mock_character))
        
    def test_default_castle_boss(self):
        """ Verifty the default boss is Bowser """
        self.assertEqual(self.castle.get_boss(), "Bowser")
        
    def test_default_castle_world(self):
        """ Verify the default world is Grass Land """
        self.assertEqual(self.castle.get_world(), "Grass Land")

    # Mock a class method
    @mock.patch.object(Castle, 'get_boss')
    def test_mock_castle_boss(self, mock_get_boss):
        mock_get_boss.return_value = "Hammer Bro"
        self.assertEqual(self.castle.get_boss(), "Hammer Bro")
        self.assertEqual(self.castle.get_world(), "Grass Land")
        
    # Mock an instance
    @mock.patch(__name__+'.Castle')
    def test_mock_castle(self, MockCastle):
        instance = MockCastle
        instance.get_boss.return_value = "Toad"
        instance.get_world.return_value = "Desert Land"
        self.castle = Castle
        self.assertEqual(self.castle.get_boss(), "Toad")
        self.assertEqual(self.castle.get_world(), "Desert Land")
        
    # Mock an instance method
    def test_mock_castle_instance_method(self):
        # Boss is still Bowser
        self.assertNotEqual(self.castle.get_boss(), "Koopa Troopa")
        # Set a return_value for the get_boss method
        self.castle.get_boss = mock.Mock(return_value = "Koopa Troopa")
        # Boss is Koopa Troopa now
        self.assertEqual(self.castle.get_boss(), "Koopa Troopa")
        
    def test_castle_with_more_bosses(self):
        multi_boss_castle = mock.Mock()
        # Set a list as side_effect for the get_boss method
        multi_boss_castle.get_boss.side_effect = ["Goomba", "Boo"]
        # First value is Goomba
        self.assertEqual(multi_boss_castle.get_boss(), "Goomba")
        # Second value is Boo
        self.assertEqual(multi_boss_castle.get_boss(), "Boo")
        # Third value does not exist and raises a StopIteration
        self.assertRaises(StopIteration, multi_boss_castle.get_boss)
         
    def test_calls_to_castle(self):
        self.castle.access = mock.Mock()
        self.castle.access.return_value = "No access"
        # We should retrieve no access for everybody
        self.assertEqual(self.castle.access('Let me in'), "No access")
        self.assertEqual(self.castle.access('Let me in, please'), "No access")
        self.assertEqual(self.castle.access('Let me in, please sir!'), "No access")
        # Verify the length of the arguments list
        self.assertEqual(len(self.castle.access.call_args_list), 3)

Run the test suite

In [102]:
import sys
suite = unittest.TestLoader().loadTestsFromTestCase(CharacterTestClass)
unittest.TextTestRunner(verbosity=4,stream=sys.stderr).run(suite)
test_calls_to_castle (__main__.CharacterTestClass) ... ok
test_castle_with_more_bosses (__main__.CharacterTestClass) ... ok
test_default_castle_boss (__main__.CharacterTestClass)
Verifty the default boss is Bowser ... ok
test_default_castle_world (__main__.CharacterTestClass)
Verify the default world is Grass Land ... ok
test_mock_access_denied (__main__.CharacterTestClass)
Access denied for star powerup ... ok
test_mock_access_granted (__main__.CharacterTestClass)
Access granted for mushroom powerup ... ok
test_mock_castle (__main__.CharacterTestClass) ... ok
test_mock_castle_boss (__main__.CharacterTestClass) ... ok
test_mock_castle_instance_method (__main__.CharacterTestClass) ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.016s

OK
Out[102]:
<unittest.runner.TextTestResult run=9 errors=0 failures=0>
In [103]:
class CharacterCastleTestClass(unittest.TestCase):
    """ Defines the tests for the Character and Castle class together """
    @mock.patch(__name__+'.Castle')
    @mock.patch(__name__+'.Character')
    def test_mock_castle_and_character(self, MockCharacter, MockCastle):
        # Note the order of the arguments of this test
        MockCastle.name = 'Mocked Castle'
        MockCharacter.name = 'Mocked Character'
        self.assertEqual(Castle.name, 'Mocked Castle')
        self.assertEqual(Character.name, 'Mocked Character')
    
    def test_fake_powerup(self):
        character = Character("Sentinel Character")
        character.powerup = mock.Mock()
        character.powerup.return_value = mock.sentinel.fake_superpower
        self.assertEqual(character.powerup(), mock.sentinel.fake_superpower)
        
    def test_castle_with_more_powerups(self):
        self.castle = Castle('Beautiful Castle')
        multi_characters = mock.Mock()
        # Set a list as side_effect for the get_boss method
        multi_characters.get_powerup.side_effect = ["mushroom", "star"]
        # First value is mushroom
        self.assertEqual(multi_characters.get_powerup(), "mushroom")
        # Second value is star
        self.assertEqual(multi_characters.get_powerup(), "star")
        # Third value does not exist and raises a StopIteration
        self.assertRaises(StopIteration, multi_characters.get_powerup)
In [104]:
suite = unittest.TestLoader().loadTestsFromTestCase(CharacterCastleTestClass)
unittest.TextTestRunner(verbosity=2,stream=sys.stderr).run(suite)
test_castle_with_more_powerups (__main__.CharacterCastleTestClass) ... ok
test_fake_powerup (__main__.CharacterCastleTestClass) ... ok
test_mock_castle_and_character (__main__.CharacterCastleTestClass) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.006s

OK
Out[104]:
<unittest.runner.TextTestResult run=3 errors=0 failures=0>