Tutorial [1]#
Basics#
As said before, with Pymox you should set expectations and then enter in replay mode. Here is a basic example:
class Duck:
def quack(self, times=1):
return ['quack'] * times
def walk(self):
return ['walking']
def walk_and_quack(self, times=1):
return self.walk() + self.quack(times=times)
Here is a Duck
class. Let’s play with our 🦆 and Pymox!
import mox
class TestDuck:
def test_quack(self):
m = mox.Mox()
m_duck = m.CreateMock(Duck)
# expects quack to be called with `times=1`
m_duck.quack(times=1).returns(['new quack'])
m.replay_all()
assert m_duck.quack(times=1) == ['new quack']
m.verify_all()
Let’s change the test a little bit:
# [...]
def test_quack_2(self):
m = mox.Mox()
m_duck = m.CreateMock(Duck)
# expects quack to be called with `times=1`
m_duck.quack(times=1).returns(['new quack'])
m.replay_all()
assert m_duck.walk() == ['walking']
assert m_duck.quack(times=1) == ['new quack']
m.verify_all()
The test above will fail with the following error:
E mox.mox.UnexpectedMethodCallError: Unexpected method call. unexpected:- expected:+
E - Duck.walk() -> None
E + Duck.quack(times=1) -> ['new quack']
Since you expected quack to be called and walk was called instead. You can add an expectation for walk:
def test_quack_3(self):
m = mox.Mox()
m_duck = m.CreateMock(Duck)
# expects quack to be called with `times=1`
m_duck.quack(times=1).returns(['new quack'])
m_duck.walk().returns(['pretending to be walking'])
m.replay_all()
assert m_duck.quack(times=1) == ['new quack']
assert m_duck.walk() == ['pretending to be walking']
m.verify_all()
You can also stub out quack
method only and mox won’t care about the other methods:
def test_quack_4(self):
m = mox.Mox()
duck = Duck()
m.stubout(duck, 'quack')
"""
You can also do with the class:
m.stubout(Duck, 'quack')
"""
# expects quack to be called with `times=1`
duck.quack(times=1).returns(['new quack'])
m.replay_all()
assert duck.quack(times=1) == ['new quack']
assert duck.walk() == ['walking']
m.verify_all()
The order matters, so if you do:
def test_quack_5(self):
m = mox.Mox()
m_duck = m.CreateMock(Duck)
# expects quack to be called with `times=1`
m_duck.quack(times=1).returns(['new quack'])
m_duck.walk().returns(['pretending to be walking'])
m.replay_all()
assert m_duck.walk() == ['pretending to be walking']
assert m_duck.quack(times=1) == ['new quack']
m.verify_all()
It fails with:
E mox.mox.UnexpectedMethodCallError: Unexpected method call. unexpected:- expected:+
E - Duck.walk() -> None
E + Duck.quack(times=1) -> ['new quack']
To fix that you can use any_order()
:
def test_quack_6(self):
m = mox.Mox()
m_duck = m.CreateMock(Duck)
# expects quack to be called with `times=1`
m_duck.quack(times=1).any_order().returns(['new quack'])
m_duck.walk().any_order().returns(['pretending to be walking'])
m.replay_all()
assert m_duck.walk() == ['pretending to be walking']
assert m_duck.quack(times=1) == ['new quack']
Comparators#
You can use comparators when you are unsure of the arguments of a method call.
def test_quack_7(self):
m = mox.Mox()
duck = Duck()
m.stubout(Duck, 'quack')
def validate_arg(arg):
if arg in [1, 2, 3]:
return True
return False
duck.quack(times=mox.is_a(int)).returns(['new quack'])
duck.quack(times=mox.not_(mox.is_(4))).returns(['new quack'])
duck.quack(times=mox.func(validate_arg)).returns(['new quack'])
duck.quack(times=mox.or_(mox.Is(1), mox.is_(2), mox.is_(3))).returns(['new quack'])
duck.quack(times=mox.ignore_arg()).returns(['new quack'])
duck.quack(times=mox.is_almost(1.00003, places=4)).returns(['new quack'])
m.replay_all()
assert duck.quack(times=random.choice([1, 2, 3])) == ['new quack']
assert duck.quack(times=random.choice([1, 2, 3])) == mox.in_('new quack')
assert duck.quack(times=random.choice([1, 2, 3]))[0] == mox.str_contains('quack')
assert duck.quack(times=random.choice([1, 2, 3])) == mox.same_elements_as({'new quack'})
assert duck.quack(times=random.choice([1, 2, 3])) == ['new quack']
assert duck.quack(times=1) == ['new quack']
m.verify_all()
All the assertions for the test above should pass.
There are other cool comparators, like: and
, contains_attribute_value
, contains_key_value
.
For more comparators, see: https://pymox.readthedocs.io/en/latest/reference.html#comparators
Remember#
It’s possible to also remember a value that might be changed in your code. See the test below:
def test_quack_8(self):
class StopQuackingDuck:
def _do_quack(self, choices=None):
return choices
def quack(self, choices=[], less=False):
if less:
choices.pop()
self._do_quack(choices=choices)
m = mox.Mox()
duck = StopQuackingDuck()
m.stubout(StopQuackingDuck, '_do_quack')
choices_1 = mox.value()
choices_2 = mox.value()
duck._do_quack(choices=mox.remember(choices_1))
duck._do_quack(choices=mox.remember(choices_2))
duck._do_quack(choices=mox.remember(choices_2))
duck._do_quack(choices=mox.remember(choices_2))
all_choices = ['quack', 'new quack', 'newest quack']
m.replay_all()
duck.quack(all_choices, less=False)
assert choices_1 == ['quack', 'new quack', 'newest quack']
duck.quack(all_choices, less=True)
assert choices_2 == ['quack', 'new quack']
duck.quack(all_choices, less=True)
assert choices_2 == ['quack']
duck.quack(all_choices, less=True)
assert choices_2 == []
m.verify_all()
Other#
You can also make a method return a different value the second time it’s called:
def test_walk_and_quack_0(self):
m = mox.Mox()
duck = Duck()
m.stubout(Duck, 'quack')
duck.quack(times=1).returns(['new quack'])
duck.quack(times=1).returns(['newest quack'])
m.replay_all()
assert duck.walk_and_quack() == ['walking', 'new quack']
But since we didn’t use m.verify_all(), it didn’t require the second call to happen. Let’s add the verify and see what happens:
def test_walk_and_quack_1(self):
m = mox.Mox()
duck = Duck()
m.stubout(Duck, 'quack')
duck.quack(times=1).returns(['new quack'])
duck.quack(times=1).returns(['newest quack'])
m.replay_all()
assert duck.walk_and_quack() == ['walking', 'new quack']
m.verify_all()
It fails with:
E mox.mox.ExpectedMethodCallsError: Verify: Expected methods never called:
E 0. Duck.quack.__call__(times=1) -> ['newest quack']
Let’s fix it by adding a second call:
def test_walk_and_quack_2(self):
m = mox.Mox()
duck = Duck()
m.stubout(Duck, 'quack')
duck.quack(times=1).returns(['new quack'])
duck.quack(times=1).returns(['newest quack'])
m.replay_all()
assert duck.walk_and_quack() == ['walking', 'new quack']
assert duck.walk_and_quack() == ['walking', 'new quack']
m.verify_all()
Now you get the following error, since in the second time it returns [‘newest quack’].
E AssertionError: assert ['walking', 'newest quack'] == ['walking', 'new quack']
E At index 1 diff: 'newest quack' != 'new quack'
E Full diff:
E - ['walking', 'new quack']
E + ['walking', 'newest quack']
E ? +++
Let’s fix it:
def test_walk_and_quack_3(self):
m = mox.Mox()
duck = Duck()
m.stubout(Duck, 'quack')
duck.quack(times=1).returns(['new quack'])
duck.quack(times=1).returns(['newest quack'])
m.replay_all()
assert duck.walk_and_quack() == ['walking', 'new quack']
assert duck.walk_and_quack() == ['walking', 'newest quack']
m.verify_all()
Let’s now see how we can mock and assert calls in the context of a loop:
def test_walk_and_quack_4(self):
m = mox.Mox()
duck = Duck()
m.stubout(Duck, 'quack')
duck.quack(times=1).returns(['new quack'])
m.replay_all()
assert duck.walk() == ['walking']
for _ in range(3):
assert duck.walk_and_quack() == ['walking', 'new quack']
m.verify_all()
If you run the test above, you get the following:
E mox.mox.UnexpectedMethodCallError: Unexpected method call Duck.quack.__call__(times=1) -> None
Let’s fix by using the multiple_times
group.
def test_walk_and_quack_5(self):
m = mox.Mox()
duck = Duck()
m.stubout(Duck, 'quack')
duck.quack(times=1).multiple_times().returns(['new quack'])
m.replay_all()
assert duck.walk() == ['walking']
for _ in range(3):
assert duck.walk_and_quack() == ['walking', 'new quack']
m.verify_all()
If you know exactly how many calls are made, you can add an argument: .multiple_times(3)
.