A test double library for python.
from unittest import TestCase
from tdubs import Stub, Spy, calling, verify
# The thing I want to test:
class Greeter(object):
# tdubs works best with code that has injectable dependencies:
def __init__(self, prompter=None, printer=None):
self.prompter = prompter or input
self.printer = printer or print
def greet(self, greeting):
fname = self.prompter('First name:')
lname = self.prompter('Last name:')
self.printer('%s, %s %s!' % (greeting, fname, lname))
class TestGreeter(TestCase):
def setUp(self):
# use stubs to provide canned responses to queries:
prompter = Stub()
calling(prompter).passing('First name:').returns('Justin')
calling(prompter).passing('Last name:').returns('Blake')
# use spies to verify commands:
self.printer = Spy()
self.greeter = Greeter(prompter, self.printer)
def test_prints_greeting_to_full_name(self):
self.greeter.greet('Greetings')
verify(self.printer).called_with('Greetings, Justin Blake!')
>>> from tdubs import Stub >>> my_stub = Stub('my_stub')
All attribute and key lookups on a stub will return another stub:
>>> my_stub.some_attribute <Stub name='some_attribute' ...>
You can define explicit attributes:
>>> my_stub.some_attribute = 'some value' >>> my_stub.some_attribute 'some value' >>> my_stub = Stub('my_stub', predefined_attribute='predefined value') >>> my_stub.predefined_attribute 'predefined value'
Key lookups work the same way as attribute lookups:
>>> my_stub['some_key'] <Stub name='some_key' ...> >>> my_stub['some_key'] = 'some dict value' >>> my_stub['some_key'] 'some dict value' >>> my_stub['another_key'].foo = 'foo' >>> my_stub['another_key'].foo 'foo'
You must explicitly make your stub callable. This is to avoid false positives in tests for logic that may depend on the truthiness of a return value.
>>> my_stub() Traceback (most recent call last): ... TypeError: <Stub name='my_stub' ...> is not callable ... >>> from tdubs import calling >>> calling(my_stub).returns('some return value') >>> my_stub() 'some return value'
Since attribute lookups return a stub by default, you can treat your stub like an object with callable methods:
>>> calling(my_stub.some_method).returns('some method result') >>> my_stub.some_method() 'some method result'
You can stub calls with specific arguments:
>>> calling(my_stub).passing('some argument').returns('specific value') >>> my_stub('some argument') 'specific value'
When you do, the original stubs are retained:
>>> my_stub() 'some return value'
Instead of giving your callable a return value, you can tell it to raise an exception:
>>> calling(my_stub.kaboom).raises(Exception('Kaboom!')) >>> my_stub.kaboom() Traceback (most recent call last): ... Exception: Kaboom!
Spies have all the functionality of stubs, but they are callable by default, and will record calls for verification. So if you need to verify calls, use a spy (see Stubs vs. Spies for more details):
>>> from tdubs import Spy >>> my_spy = Spy('my_spy')
Any call to a spy will return a new spy:
>>> my_spy() <Spy ...> >>> my_spy('arg1', 'arg2', foo='bar') <Spy ...>
All calls to a spy are recorded:
>>> from tdubs import calls >>> calls(my_spy) [<Call args=() kwargs={}>, <Call args=('arg1', 'arg2') kwargs={'foo': 'bar'}>]
You can verify that something was called:
>>> from tdubs import verify >>> verify(my_spy).called() True >>> new_spy = Spy('new_spy') >>> verify(new_spy).called() Traceback (most recent call last): ... tdubs.verifications.VerificationError: expected <Spy ...> to be called, but it wasn't
You can verify that it was called with specific arguments:
>>> verify(my_spy).called_with('arg1', 'arg2', foo='bar') True >>> verify(my_spy).called_with('foo') Traceback (most recent call last): ... tdubs.verifications.VerificationError: expected <Spy ...> to be called with ('foo'), ...
You can also verify that it was not called:
>>> verify(new_spy).not_called() True >>> new_spy() <Spy ...> >>> verify(new_spy).not_called() Traceback (most recent call last): ... tdubs.verifications.VerificationError: expected <Spy ...> to not be called, but it was
Or that it was not called with specific arguments:
>>> verify(new_spy).not_called_with('foo') True >>> new_spy('foo') <Spy ...> >>> verify(new_spy).not_called_with('foo') Traceback (most recent call last): ... tdubs.verifications.VerificationError: expected <Spy ...> to not be called with ('foo'), but it was
You should use Stub
when you are testing behavior that depends on the state
or return value of some other object. For example, the behavior of the
Greeter
in the Example above depends on the return value of
prompter
, so I'm using a stub.
Stubs are not callable by default. You must explicitly stub a return value if you expect it to be called. This is to avoid false positives in your tests for behavior that may depend on the truthiness of that call.
Spies are callable by default, because they are designed to record calls for
verification after execution. You should use Spy
when you only need to
verify that something was called. For example, I need to verify whether or not
printer
was called with the correct string, so I'm using a spy.
You can think of it this way: use Stub
for queries, and Spy
for
commands. If the separation isn't clear, spend some time thinking about your
design. Would it be better with distinct queries and commands? (If you really
need both, use Spy
, since it extends Stub
).
Further reading:
Note: in the articles above, the concepts attributed to "mocks" also apply to "spies" as they are implemented in tdubs.
The Little Mocker is a great article by Uncle Bob explaining the different types of test doubles and when you would use them. So why does tdubs only implement Stub and Spy?
Short answer: you don't need a library to use the rest.
Here's a rundown of what's missing, when you would use them, and how to implement them:
- Dummies: For stand-ins that don't matter to the behavior being tested.
Example: extraneous call arguments. Use
object()
. - Fakes: For situations where a double needs some behavior, but it can be faked. Example: an in-memory repository. Code it from scratch.
- Mocks: Like spies, but call expectations are assigned before execution. Just use a spy (so your tests read as setup => execute => verify).
I personally try to avoid doing this,
but sometimes the trade-offs make sense, so tdubs has a patch
module with
thin wrappers around unittest.mock.patch
. They work the same way, but give
you a tdubs.Stub
or tdubs.Spy
instead of a
unittest.mock.MagicMock
:
>>> from tdubs import patch >>> with patch.stub('%s.open' % __name__) as open_stub: ... calling(open_stub).passing('file', 'r').returns('file handle') ... open('file', 'r') 'file handle' >>> with patch.spy('%s.print' % __name__) as print_spy: ... print('Hello!') <Spy ...> >>> verify(print_spy).called_with('Hello!') True
Since these wrap unittest.mock.patch
, you can see
python's patch documentation
for full usage information.
Python 3 already has unittest.mock
, and there are several other third-party
test double packages, but none felt like the right fit for how I like to TDD.
This is what I wanted out of a test double library:
The ability to treat a double as a callable with return values specific to the arguments passed in. This is so I can treat stubs as pure stubs, without needing to verify I passed the right arguments to my query methods. You can see that in action in the example above.
The ability to verify calls after they are made, without setting up expectations first. This is so my tests read like a story:
# set up: my_spy = Spy() # execute: my_func(my_spy) # verify: verify(my_spy).called()
Test doubles with zero public attributes from the library. This is to avoid conflicts with the object being replaced in tests. For example:
Since all attributes on a mock return a new mock, the following assertion will always evaluate to True:
>>> try: ... from unittest import mock ... except ImportError: ... import mock ... >>> mock.Mock().asssert_called_with('foo') # oops! <Mock ...>
Notice the typo? If not, you may get a false positive in your test.
tdubs avoids this by using a new object for verifications:
>>> from tdubs import Spy, verify >>> verify(Spy()).callled_with('foo') # oops! Traceback (most recent call last): ... AttributeError: 'Verification' object has no attribute 'callled_with'
Notice the typo? If not, it doesn't matter. Python noticed!
I also like the distinction between stubs and spies (see Stubs vs. Spies), but it's not one of the reasons I originally decided to write tdubs.
pip install tdubs
Clone the project.
Install dev dependencies:
pip install -e .[dev]
Run the tests:
nosetests
Lint and test the code automatically when changes are made (see tube.py
):
stir