Welcome to Ditto’s documentation!¶
Class mocking framework for use with unit testing¶
Summary¶
This module is meant to implement a “more” elegant class mocking framework for python than what I can find by searching on Google. And by “elegant” I mean more like Google’s GMock. You might even say this is a port of GMock, although I would say it’s still got some pythonic flare.
How To Use¶
Here’s a general guide to how you can use the framework.
Instantiating Mock Instances¶
Given some class that you want to mock (call it `Foo), declare a mocked instance of that class by doing the following:
my_instance = Mock(Foo)
That is, pass the class that you want to mock to the constructor of Mock. It will automagically make my_instance have the same interface as Foo, except the methods will be mocked rather than real. (Note that there’s no need to ever define an actual class.) By default, all methods that don’t feel like python secret methods are mocked up. In other words, two mocked instances are “equal” based on the __eq__ method in the Mock class. That method isn’t “mocked” based on seeing it in the original class. That’s because __eq__ isn’t a method that will be mocked up by default. Mocking every single method would essentially make Mock instances completely unusable, because the mocks, themselves, lose most of their identity, and they still need to exist as viable python objects.
The framework doesn’t do a particularly good job of picking which methods to mock and which to not, but that’s actually not that big of a deal – any formula we choose would be bad in a certain set of cases. That’s why we let you define your own formula. If you’d like to control which methods get a mock or not, just declare a function that decides this, and then register it when you create your mock object:
def pick_functions(class, method):
return method in ['foo', 'bar', 'baz']
my_instance = Mock(Foo, _method_selector=pick_functions)
As another example, the following function mocks every single method on a mocked instance:
def pick_functions(class, method_name):
'''Return True if we should mock method_name in class'''
return True
This example is not a useful one, though – don’t ever just return True. __class__ is unmockable, because python will need that to actually point to the Mock class. By the way, if you’re trying to mock __class__ you’re doing something wrong, so this isn’t exactly a heinous restriction. It just means there’s no easy way to say “give me everything,” because “everything” is never a good idea.
Creating Expectations¶
Rather than have the behavior you’d expect, each mocked method on a mocked instance checks to make sure that we were expecting such a method call (this method, these arguments, at this time) to happen. In order for it to make this decision, you must give it a set of expectations to operate under.
To “expect” that a method on my_instance (call it bar) will be called with a given set of parameters (say 1, 2, 3), do the following:
my_instance.foo.expect(1, 2, 3)
If you don’t do this, calling the method (at this time) will not be expected, and will raise an AssertionError. (Note that the appropriate way to assert that a method should never be called is to never call its expect method.) The above snippet tells the mock framework to expect the following:
my_instance.foo(1, 2, 3)
but only with these arguments. Any other arguments will be unexpected, and will raise an AssertionError.
Expectations retire when they’ve been satisfied. The mock framework holds a list of all the expectations that are still waiting to be satisfied. At the end of your program’s run, you might want to assert that there are no more expectations waiting to be met. This is a way of asserting that your code under test actually did all the stuff you wanted it to do.
In order to do this, you need to understand contexts. When you create a Mock instance, as demonstrated above, the framework will create that instance in the default context. Expectations created off that mock instance will be added to the list of expectations in that default context. The default context is a singleton, which can be accessed as follows:
from ditto import default_context
There are two useful functions to use on a context. assert_no_more_expectations() will raise AssertionError if your test code hasn’t met all the expectations you’ve created. retire_all_expectations() clears the list, so that you can start a new round of testing.
If you’d like to separate expectations into multiple contexts, so that you can assert that different sets of expectations have been met at different points during your test, you can specify custom contexts by doing the following:
c1 = Context()
c2 = Context()
m1 = Mock(Foo, _context=c1)
m2 = Mock(Foo, _context=c2)
Obviously you can operate on c1 and c2 just like you can on default_context.
Expecting Indefinite Arguments¶
Brittle tests suck. It’s probably the case that you’ll have to go tweak something in a unit test when you modify the code that it tests, but we’d like that to be as little as is reasonable. One way of helping with this is to create expectations that are only as explicit as is required for the code to be considered correct. (As an example, if your HTTP proxy starts tacking on an X-Foo header, and that header really isn’t important, make sure your relevant mocked classes won’t fail when it starts to add that header.)
It might be the case, for instance, that all you really care about is that a method is called with any arguments, or with three arguments, or that these arguments are all strings, or that these string arguments all have the substring “foo” in them.
To express such inexact expectations, use Hamcrest matchers. python-hamcrest is a literal port of the Java-based Hamcrest library into Python. Think of these matchers as “regular expressions” for all different kinds of python values (not just strings).
To use, wrap any hamcrest matcher instance in the matches() function from this module:
my_instance.foo.expect(matches(hamcrest.less_than(3)))
You can also wrap any instance that declares the method matches(other_thing) with matches(), not just those found in hamcrest.
Changing Expectations¶
By default, expectations retire after being satisfied a single time, can happen in any order, and return None to the caller when they’re satisfied. You can change all of these things by calling various methods on an expectation instance, which expect() returns:
my_instance.foo.expect().returns(23).times(3).optional()
my_instance.foo.expect().raises(Exception).infinite_times()
You can set whether an expectation returns a value or raises an error by using returns and raises. You can make assert_no_more_expectations skip over your expectation by using optional. You can set the number of times you want your expectation have to be met before it retires using times (or you can make it never retire by using infinite_times.
Another important way you can change an expectation is by putting it in a sequence. By default, expectations that are active are active for all possible method calls in your context. That is, it doesn’t really matter what order you created the expectations in. Obviously you might want to test that the order of method calls in your code follows a tighter restriction. Do this by dropping expectations into sequences:
s = Sequence()
my_instance.foo.expect(5).in_sequence(s)
my_instance.foo.expect(6).in_sequence(s)
This asserts that the expectation of foo being called with 6 is only active after the expectation that foo is called with 5 retires. In other words, you have to call them in the order of the sequence for the test to pass.
You can put expectations in more than one sequence, and you can put expectations from more than one mocked object into the same sequence:
s1 = Sequence()
s2 = Sequence()
# Expectation A
my_instance.foo.expect(5).in_sequence(s1)
# Expectation B
my_other_instance.foo.expect(6).in_sequence(s1).in_sequence(s2)
# Expectation C
my_other_instance.bar.expect(6).in_sequence(s1)
# Expectation D
my_instance.bar.expect(8).in_sequence(s2)
This declares two sequences that, together, contain four expectations (labeled A, B, C, D). Those sequences, if I were to print them out, would look like this:
s1 = (A, B, C,)
s1 = (B, D,)
For this context, those are the only two sequences of expectations that will result in a passing test. The framework will throw an error as soon as there’s no situation that could possibly result in a passing test. For instance, D before anything else, or A followed by D, or B followed by C. It doesn’t have to get to the very end to realize that there’s no way the test can pass. This is useful because it helps make the traceback from the actual problematic call site visible.
Applicative Declaration¶
Often, it’s very useful to declare test data inside a huge applicative python structure. Here’s an example:
my_data = [
{'id': 1,
'segment': Mock(Segment),}
]
It’s really hard to do that if you have to go back and set expecations on all the mock objects inside such a huge structure. In the above example, it would appear, on the surface, that I might have to write code that iterates through my huge structure, finds all the mock objects, and individually declares expectations on them. Or, maybe I’d have to just delegate to some function:
# XXX: This is NOT the way to do it!!
def create_segment():
m = Mock(PdtsSeg)
m.send.expect().times(5)
m.send_new_data.expect(matches(anything)).times(3)
return m
my_data = [
{'id': 1,
'segment': create_segment(),}
]
The solution this framework proposes can be colloquially described as “using back pointers.” Expectations point to their methods with the method attribute, and methods point to their mock instance with the mock keyword. And every method on an expectation returns the expectation right back to you. So you can stack calls, like so:
# XXX: Wow, that's a lot better.
my_data = [
{'id': 1,
'segment':
Mock(PdtsSeg).send.expect().times(5).method.mock \
.send_new_data(matches(anything)).times(3).method.mock
}
]
So what does all of this mean? It’s entirely possible that every single mock instance you’ll ever create can be created with a single python expression, and without having to declare a class. Beat that, googlemock. ;)
To Do¶
There’s still a bunch to do.
- Currently, you have to manually retire expectations and check that all expectations were met. Surely there’s a better way.
- Should there be an easy way to make expectations from the same mocked instance go into different contexts?