Browse the source code, get support and notify bugs in the issue tracker.
doublex-1.8.2/pydoubles-site/overview 0000644 0001750 0001750 00000010511 12435722065 017005 0 ustar piotr piotr [migrated]
What is pyDoubles?
pyDoubles is a test doubles framework for the Python platform. Test doubles frameworks are also called mocking frameworks. pyDoubles can be used as a testing tool or as a Test Driven Development tool.
It generates stubs, spies, and mock objects using a fluent interface that will make your unit tests more readable. Moreover, it's been designed to make your tests less fragile when possible.
The development of pyDoubles has been completely test-driven from scratch. The project is under continuous evolution, but you can extend the framework with your own requirements. The code is simple and well documented with unit tests.
What is doublex?
doublex is a new doubles framework that optionally provides the pyDoubles legacy API. It supports all the pyDoubles features and some more that can not be easely backported. If you are a pyDoubles user you can run your tests using doublex.pyDoubles module. However, we recommed the native doublex API for your new developments.
Supported test doubles
Find out what test doubles are according to Gerard Meszaros. pyDoubles offers mainly three kind of doubles:
Stub
Replaces the implementation of one or more methods in the object instance which plays the role of collaborator or dependency, returning the value that we explicitly write down in the test. A stub is actually a method but it is also common to use the noun stub for a class with stubbed methods. The stub does not have any kind or memory.
Stubs are used mainly for state validation or along with spies or mocks.
Spy
Replaces the implementation as a stub does, but it is also able to register and remember what methods are called during the test execution and how they are invoked.
They are used for interaction/behavior verification.
Mock
Contains the same features than the Stub and therefore the Spy, but it is very strict in the behavior specification it should expect from the System Under Tests. Before calling any method in the mock object, the framework should be told (in the test) which methods we expect to be called in order for them to succeed. Otherwise, the test will fail with an "UnexpectedBehavior" exception.
Mock objects are used when we have to be very precise in the behavior specification. They usually make the tests more fragile than a spy but still are necessary in many cases. It is common to use mock objects together with stubs in tests.
New to test doubles?
A unit test is comprised of three parts: Arrange/Act/Assert or Given/When/Then or whatever you want to call them. The scenario has to be created, exercised, and eventually we verify that the expected behavior happened. The test doubles framework is used to create the scenario (create the objects), and verify behavior after the execution but it does not make sense to invoke test doubles' methods in the test code. If you call the doubles' methods in the test code, you are testing the framework itself, which has been already tested (better than that, we crafted it using TDD). Make sure the calls to the doubles' methods happen in your production code.
Why another framework?
pyDoubles is inspired in mockito and jMock for Java, and also inspired in Rhino.Mocks for .Net. There are other frameworks for Python that work really well, but after some time using them, we were not really happy with the syntax and the readability of the tests. Fragile tests were also a problem. Some well-known frameworks available for Python are: mocker, mockito-python, mock, pymox.
pyDoubles is open source and free software, released under the Apache License Version 2.0
Take a look at the project's blog
doublex-1.8.2/pydoubles-site/pydoubles-documentation 0000644 0001750 0001750 00000027177 12435722065 022034 0 ustar piotr piotr [migrated]
class SimpleExample(unittest.TestCase):
def test_ask_the_sender_to_send_the_report(self):
sender = spy(Sender())
service = SavingsService(sender)
service.analyze_month()
assert_that_method(sender.send_email).was_called(
).with_args('reports@x.com', ANY_ARG)
Import the framework in your tests
import unittest
from doublex.pyDoubles import *
If you are afraid of importing everything from the pyDoubles.framework module, you can use custom imports, although it has been carefully designed to not conflict with your own classes.
import unittest
from doublex.pyDoubles import stub, spy, mock
from doublex.pyDoubles import when, expect_call, assert_that_method
from doublex.pyDoubles import method_returning, method_raising
You can import Hamcrest matchers which are fully supported:
from hamcrest import *
Which doubles do you need?
You can choose to stub out a method in a regular object instance, to stub the whole object, or to create three types of spies and two types of mock objects.
Stubs
There are several ways to stub out methods.
Stub out a single method
If you just need to replace a single method in the collaborator object and you don't care about the input parameters, you can stub out just that single method:
collaborator = Collaborator() # create the actual object
collaborator.some_calculation = method_returning(10)
Now, when your production code invokes the method "some_calculation" in the collaborator object, the framework will return 10, no matter what parameters are passed in as the input.
If you want the method to raise an exception when called use this:
Now, when your production code invokes "some_calculation" method, the stub will return 10, no matter what arguments are passed in.
You can also specify different return values depending on the input:
This means that "collaborator.some_calculation(5)" will return 10, and that it will return 20 when the input is 10. You can define as many input/output specifications as you want.
The method "some_other_method" will return 10 as long as the first parameter is 5, no matter what the second parameter is. You can use any combination of "ANY_ARG" arguments. But remember that if all of them are ANY, you shouldn't specify the arguments, just use this:
So this call: collaborator.some_other_method(10) wil return 10.
Matchers
You can also specify that arguments will match a certain function. Say that you want to return a value only if the input argument contains the substring "abc":
In the last release, pyDoubles matchers are just aliases for the hamcrest counterparts. See release notes.
Hamcrest Matchers
Since pyDoubles v1.2, we fully support Hamcrest matchers.
They are used exactly like pyDoubles matchers:
from hamcrest import *
from doublex.pyDoubles import *
def test_has_entry_matcher(self):
list = {'one':1, 'two':2}
when(self.spy.one_arg_method).with_args(
has_entry(equal_to('two'), 2)).then_return(1000)
assert_that(1000, equal_to(self.spy.one_arg_method(list)))
def test_all_of_matcher(self):
text = 'hello'
when(self.spy.one_arg_method).with_args(
all_of(starts_with('h'), instance_of(str))).then_return(1000)
assert_that(1000, equal_to(self.spy.one_arg_method(text)))
Note that the tests above are just showhing the pyDoubles framework working together with Hamcrest, they are not good examples of unit tests for your production code.
The method assert_that comes from Hamcrest, as well as the matchers: has_entry, equal_to, all_of, starts_with, instance_of.
Notice that all_of and any_of, allow you to define more than one matcher for a single argument, which is really powerful.
For more informacion on matchers, read this blog post.
Stub out the whole unexisting object
If the Collaborator class does not exist yet, or you don't want the framework to check that the call to the stub object method matches the actual API in the actual object, you can use an "empty" stub.
The framework is creating the method "alpha_operation" dynamically
and making it return "whatever".
The use of empty_stub, empty_spy or empty_mock is not recommended because you lose the API match check. We only use them as the construction of the object is too complex among other circumstances.
Spies
Please read the documentation above about stubs, because the API to
define method behaviors is the same for stubs and spies. To create
the object:
collaborator = spy(Collaborator())
After the execution of the system under test, we want to validate
that certain call was made:
That will make the test pass if method "send_email" was invoked one or more times, no matter what arguments were passed in.
We can also be precise about the arguments:
Any other call to any method in the "sender" double will return "None" and will not interrupt the test. We are not telling all that happens between the sender and the SUT, we are just asserting on what we want to verify.
The ANY_ARG matcher can be used to verify the call as well:
You can also create an "empty_spy" to not base the object in a
certain instance:
sender = empty_spy()
The ProxySpy
There is a special type of spy supported by the framework which
is the ProxySpy:
collaborator = proxy_spy(Collaborator())
The proxy spy will record any call made to the object but rather than replacing the actual methods in the actual object, it will execute them. So the actual methods in the Collaborator will be invoked by default. You can replace the methods one by one using the "when" statement:
Now "some_calculation" method will be a stub method but the remaining methods in the class will be the regular implementation.
The ProxySpy might be interesting when you don't know what the actual method will return in a given scenario, but still you want to check that some call is made. It can be used for debugging purposes.
The test is quite similar to the one using a spy. However the framework behaves different. If any other call to the sender is made during "some_action", the test will fail. This makes the test more fragile. However, it makes sure that this interaction is the only one between the two objects, and this might be important for you.
More precise expectations
You can also expect the call to have certain input parameters:
Which expects the method to be invoked with "wrong_email" and will return FAILURE.
Mocks are strict so if you expect the call to happen several times, be explicit with that:
As you might have seen, the "when" statement is not used for mocks, only for stubs and spies. Mock objects use the "expect_call" syntax together with the "assert_that_is_satisfied"
(instance method).
More documentation
The best and most updated documentation are the unit tests of the framework itself. We encourage the user to read the tests and see what features are supported in every commit into the source code repository:
pyDoublesTests/unit.py
You can also read about what's new in every release in the blog
doublex-1.8.2/pydoubles-site/release-notes 0000644 0001750 0001750 00000006072 12435722065 017714 0 ustar piotr piotr
Reading double attributes returns collaborator.class attribute values by default.
doublex 1.6.2
Invocation stubbed return value is now stored.
New low level spy API: double method "calls" property provides access to invocations and their argument values. Each 'call' has an "args" sequence and "kargs dictionary". This provides support to perform individual assertions and direct access to invocation argument values. (see test and doc).
doublex 1.6
First release supporting Python-3 (up to Python-3.2) [fixes issue 7].
ProxySpy propagated stubbed invocations too (see test).
doublex 1.5.1
This release includes support for asynchronous spy assertions. See this blog post for the time being, soon in the official documentation.
doublex/pyDoubles 1.5
Since this release the pyDoubles API is provided as a wrapper to doublex. However, there are small differences. pyDoubles matchers are not supported anymore, although you may get the same feature using standard hamcrest matchers. Anyway, legacy pyDoubles matchers are provided as hamcrest aliases.
In most cases the only required change in your code is the module name, that change from:
from pyDoubles.framework.*
to:
from doublex.pyDoubles import *
If you have problems migrating to the new 1.5 release or migrating from pyDoubles to doublex, please ask for help in the discussion forum or in the issue tracker.
doublex-1.8.2/pydoubles-site/support 0000644 0001750 0001750 00000001157 12435722065 016661 0 ustar piotr piotr
[ slides related to that example have gray background ]
account service
collaborators
class AccountStore:
def save(self, login, password):
[...]
def has_user(self, login):
[...]
class PasswordService:
def generate(self):
[...]
Stub
In a free stub, any method may be invoked:
class AccountTests(TestCase):
def test_account_creation(self):
with Stub() as password_service:
password_service.generate().returns('secret')
service = AccountService(store=Stub(), password_service)
service.create_user('John')
Stub
... you can set return value depending on arguments
with Stub() as stub:
stub.foo(2, 2).returns(100)
stub.foo(3, ANY_ARG).returns(200)
assert_that(stub.foo(1, 1), is_(None))
assert_that(stub.foo(2, 2), is_(100))
assert_that(stub.foo(3, 0), is_(200))
Stub
... or by hamcrest matcher
with Stub() as stub:
stub.foo(2, greater_than(4)).returns(100)
assert_that(stub.foo(2, 1), is_(None))
assert_that(stub.foo(2, 5), is_(100))
Stub
... or by composite hamcrest matcher
with Stub() as stub:
stub.foo(2, has_length(all_of(
greater_than(4), less_than(8)))).returns(1000)
assert_that(stub.foo(2, "bad"), is_(None))
assert_that(stub.foo(2, "enough"), is_(1000))
Stub
interface may be restricted to a given class:
with Stub(PasswordService) as password_service:
password_service.generate().returns('secret')
stub.generate()
stub.generate(9)
TypeError: PasswordService.generate() takes exactly 1 argument (2 given)
stub.wrong()
AttributeError: 'PasswordService' object has no attribute 'wrong'
in our AccountService test:
class AccountTests(TestCase):
def test_account_creation(self):
with Stub(PasswordService) as password_service:
password_service.generate().returns('secret')
service = AccountService(store=Stub(), password_service)
service.create_user('John')
... is 'store' really called??
we need a spy
Spy
checking double invocations: called()
store = Spy(AccountStore)
service = AccountService(store, password_service)
service.create_group('team', ['John', 'Peter', 'Alice'])
assert_that(store.save, called())
but... is really called three times?
Spy
checking called times: times()
(also with matchers)
store = Spy(AccountStore)
service = AccountService(store, password_service)
service.create_group('team', ['John', 'Peter', 'Alice'])
assert_that(store.save, called().times(3))
assert_that(store.save, called().times(greater_than(2)))
but... is really called with the right arguments?
Spy
check argument values: with_args()
(also with matchers)
store = Spy(AccountStore)
service = AccountService(store, password_service)
service.create_user('John')
assert_that(store.save, called().with_args('John', 'secret'))
assert_that(store.save, called().with_args('John', ANY_ARG))
assert_that(store.save,
called().with_args(contains_string('oh'), ANY_ARG))
assert_that(store.save,
never(called().with_args('Alice', anything())))
with ProxySpy(AccountStore()) as store:
store.has_user('John').returns(True)
service = AccountService(store, password_service)
with self.assertRaises(AlreadyExists):
service.create_user('John')
CAUTION: ProxySpy is not a true double, this invokes the actual AccountStore instance!
Mock
programming expectations
with Mock(AccountStore) as store:
store.has_user('John')
store.save('John', anything())
store.has_user('Peter')
store.save('Peter', anything())
service = AccountService(store, password_service)
service.create_group('team', ['John', 'Peter'])
assert_that(store, verify())
Mock assures these invocations (and only these) are ocurred.
def get_pass():
return "12345"
with Stub(PasswordService) as password_service:
password_service.generate().delegates(get_pass)
store = Spy(AccountStore)
service = AccountService(store, password_service)
service.create_user('John')
assert_that(store.save, called().with_args('John', '12345'))
stub delegates
delegating to iterables/generators
with Stub(PasswordService) as password_service:
password_service.generate().delegates(["12345", "mypass", "nope"])
store = Spy(AccountStore)
service = AccountService(store, password_service)
service.create_group('team', ['John', 'Peter', 'Alice'])
assert_that(store.save, called().with_args('John', '12345'))
assert_that(store.save, called().with_args('Peter', 'mypass'))
assert_that(store.save, called().with_args('Alice', 'nope'))
stubbing properties
class Collaborator(object):
@property
def prop(self):
return 1
@prop.setter
def prop(self, value):
pass
with Spy(Collaborator) as spy:
spy.prop = 2
assert_that(spy.prop, is_(2)) # double property getter invoked
spying properties
(also with matcher)
assert_that(spy, property_got('prop'))
spy.prop = 4 # double property setter invoked
spy.prop = 5 # --
spy.prop = 5 # --
assert_that(spy, property_set('prop')) # set to any value
assert_that(spy, property_set('prop').to(4))
assert_that(spy, property_set('prop').to(5).times(2))
assert_that(spy,
never(property_set('prop').to(greater_than(6))))