1 of 14

How to write your own mocking library?

Using metaprogramming in python

2 of 14

About Me

Deepak Narayana Rao

Solution Consultant @ Sahaj Software

@endeepak on www, github , twitter etc

3 of 14

What is mocking?

# Simple unit test case with mocking�def test_place_order_creates_order_in_repository():� order_repository = Mock()� order_service = OrderService(order_repository)� order = Order(product='Python Programming Book', price=300)�� order_service.place_order(order)�� order_repository.create_order.assert_called_once_with(order)

​

4 of 14

Ingredients for basic mocking

> order_repository = Mock()

> order_repository.create_order(order)

Need a way to create objects which can respond to dynamic methods

​

> order_repository.create_order.assert_called_once_with(order)

Need a way to inspect calls made to these dynamic methods

5 of 14

Metaprogramming : __getattr__

# Create objects which can respond to dynamic methods�class DynamicGreeter(object):� def __getattr__(self, attr_name):� if attr_name.startswith('greet_'):� def greeter():� print('Hello ' + attr_name.replace('greet_', ''))� return greeter� else:� raise Exception('Could not understand method ' + attr_name)

greeter = DynamicGreeter()�greeter.greet_python() # Output > Hello python�greeter.greet_everyone() # Output > Hello everyone�greeter.something_else() # Exception: Could not understand method something_else

​

6 of 14

Metaprogramming : __call__

# Ability to intercept calls

class CallableObject(object):� def __init__(self, name):� self.name = name�� def __call__(self, *args, **kwargs):� print(self.name, 'received call', args, kwargs)�

callable_object = CallableObject('awesome_method')�callable_object('hello')�# Output > awesome_method received call ('hello',) {}��callable_object('hello', to='world')�# Output > awesome_method received call ('hello',) {'to': 'world'}

7 of 14

Let us write our own mocking library

SuperMock

8 of 14

Spock V1

class Call(object):� def __init__(self, args, kwargs):� self.args = args� self.kwargs = kwargs���class SpyMethod(object):� def __init__(self, name):� self.name = name� self.calls = []�� def __call__(self, *args, **kwargs):� self.calls.append(Call(args, kwargs))

class Spock(object):� def __getattr__(self, attr_name):� spy_method = SpyMethod(attr_name)� setattr(self, attr_name, spy_method)� return spy_method

​

9 of 14

Spock V1 : Usage

from spock import Spock�from order_service import OrderService, Order��def test_place_order_creates_order_in_repository():� order_repository = Spock()� order_service = OrderService(order_repository)� order = Order(product='Python Programming Book', price=300)�� order_service.place_order(order)�� assert len(order_repository.create_order.calls) == 1� assert len(order_repository.create_order.calls[0].args) == 1� assert order_repository.create_order.calls[0].args[0] == order

​

10 of 14

Spock V1.1 - Easier way to assert

class Call(object):� # ...� def received(self, *args, **kwargs):� return (self.args, self.kwargs) == (args, kwargs)

​

class SpyMethod(object):� # ...� def was_called_with(self, *args, **kwargs):� for call in self.calls:� if(call.received(*args, **kwargs)):� return True� return False

​

# test_place_order_creates_order_in_repository

assert order_repository.create_order.was_called_with(order)

​

11 of 14

Spock V1.2 - SpyMethod to return value

Coming soon...

12 of 14

Spock V1.3 - SpyMethod to raise exception

Coming soon...

13 of 14

Next Steps : Add more features to Spock

  • SpyMethod to call through original method
  • SpyMethod to call another method on call (side effect)
  • Track the order in which spy methods were called
  • Ability to mock methods of non-mock object
  • Cleaning up mocks
  • Decorators for syntactic sugar
  • …
  • ...

​

14 of 14

Thank you !