what are doubles?
why should we use doubles?
Unit tests must isolate the SUT.
So all collaborators should be replaced.
collaborator = Collaborator()
sut = SUT(collaborator)
sut.exercise()
double = Double(Collaborator)
sut = SUT(double)
sut.exercise()
[ according to xunitpatterns.com/Test Double.html ]
class AccountService:
def __init__(self, store, password_service):
[...]
def create_user(self, login):
[...]
- raise InvalidPassword()
- raise AlreadyExists()
def create_group(self, group_name, user_names):
[...]
[ slides related to that example
have gray background ]
collaborators
class AccountStore:
def save(self, login, password):
[...]
def has_user(self, login):
[...]
class PasswordService:
def generate(self):
[...]
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')
... 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))
... 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))
... 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))
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'
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
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?
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?
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())))
check keyword argument
(also with matcher)
spy = Spy()
spy.foo(name="Mary")
assert_that(spy.foo,
called().with_args(name="Mary"))
assert_that(spy.foo,
called().with_args(name=contains_string("ar")))
meaning-full report messages!
service.create_group('team', ['John', 'Alice'])
assert_that(store.save, called().with_args('Peter'))
AssertionError:
Expected: these calls:
AccountStore.save('Peter')
but: calls that actually ocurred were:
AccountStore.has_user('John')
AccountStore.save('John', 'secret')
AccountStore.has_user('Alice')
AccountStore.save('Alice', 'secret')
propagates invocations to the collaborator
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!
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.
collaborator = Collaborator()
collaborator.foo = method_returning('bye')
assert_that(self.collaborator.foo(), is_('bye'))
collaborator.foo = method_raising(SomeException)
collaborator.foo()
SomeException:
attach additional behavior
class Observer(object):
def __init__(self):
self.state = None
def update(self, *args, **kargs):
self.state = args[0]
observer = Observer()
stub = Stub()
stub.foo.attach(observer.update)
stub.foo(2)
assert_that(observer.state, is_(2))
delegating to callables
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'))
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'))
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
(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))))