Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

A python test double library

License

NotificationsYou must be signed in to change notification settings

blaix/tdubs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

94 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build StatusCoverage StatusLatest VersionLicense

A test double library for python.

Example

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!')

Usage

Creating stubs

>>> from tdubs import Stub>>> my_stub = Stub('my_stub')

Attributes

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'

Dictionaries

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'

Callables

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'

Exceptions

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

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

Stubs vs. Spies

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.

What about the other types of test doubles?

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. Useobject().
  • 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).

Patching Imports

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.

Why?

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:

  1. 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.

  2. 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()
  3. 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.

Installation

pip install tdubs

Development

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

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp