- Notifications
You must be signed in to change notification settings - Fork1
A python test double library
License
blaix/tdubs
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A test double library for python.
fromunittestimportTestCasefromtdubsimportStub,Spy,calling,verify# The thing I want to test:classGreeter(object):# tdubs works best with code that has injectable dependencies:def__init__(self,prompter=None,printer=None):self.prompter=prompterorinputself.printer=printerorprintdefgreet(self,greeting):fname=self.prompter('First name:')lname=self.prompter('Last name:')self.printer('%s, %s %s!'% (greeting,fname,lname))classTestGreeter(TestCase):defsetUp(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)deftest_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 positivesin 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 likean 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 anexception:
>>> 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 aspy (seeStubs 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 wasnot 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 useStub
when you are testing behavior that depends on the stateor return value of some other object. For example, the behavior of theGreeter
in theExample above depends on the return value ofprompter
, so I'm using a stub.
Stubs are not callable by default. You must explicitly stub a return value ifyou expect it to be called. This is to avoid false positives in your tests forbehavior that may depend on the truthiness of that call.
Spiesare callable by default, because they are designed to record calls forverification after execution. You should useSpy
when you only need toverify that something was called. For example, I need to verify whether or notprinter
was called with the correct string, so I'm using a spy.
You can think of it this way: useStub
forqueries, andSpy
forcommands. If the separation isn't clear, spend some time thinking about yourdesign. Would it be better with distinct queries and commands? (If you reallyneed both, useSpy
, since it extendsStub
).
Further reading:
Note: in the articles above, the concepts attributed to "mocks" also apply to"spies" as they are implemented in tdubs.
The Little Mockeris a great article by Uncle Bob explaining the different types of test doublesand 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 toimplement 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 befaked. Example: an in-memory repository. Code it from scratch.
- Mocks: Like spies, but call expectations are assigned before execution. Justuse 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 apatch
module withthin wrappers aroundunittest.mock.patch
. They work the same way, but giveyou atdubs.Stub
ortdubs.Spy
instead of aunittest.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 wrapunittest.mock.patch
, you can seepython's patch documentationfor full usage information.
Python 3 already hasunittest.mock
, and there are several other third-partytest 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 tothe arguments passed in. This is so I can treat stubs as pure stubs, withoutneeding to verify I passed the right arguments to my query methods. You cansee that in action in the example above.
The ability to verify calls after they are made, without setting upexpectations 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 avoidconflicts with the object being replaced in tests. For example:
Since all attributes on a mock return a new mock, the followingassertion 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 (seeStubs 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 (seetube.py
):
stir
About
A python test double library