Doubles¶
Some very basic examples are shown below. Remember that test doubles are created to be invoked by your SUT, and a RealWorld™ test never directly invokes doubles. Here we do it that way, but just for simplicity.
Stub¶
Hint: Stubs tell you what you wanna hear.
A Stub
is a double object that may be programmed to return specified values depending
on method invocations and their arguments. You must use a context (the with
keyword)
for that.
Invocations over the Stub
must meet the collaborator interface:
from doublex import Stub, ANY_ARG, assert_that, is_
class Collaborator:
def hello(self):
return "hello"
def add(self, a, b):
return a + b
with Stub(Collaborator) as stub:
stub.hello().raises(SomeException)
stub.add(ANY_ARG).returns(4)
assert_that(stub.add(2,3), is_(4))
If you call an nonexistent method you will get an AttributeError
exception.
>>> with Stub(Collaborator) as stub:
... stub.foo().returns(True)
Traceback (most recent call last):
...
AttributeError: 'Collaborator' object has no attribute 'foo'
Wrong argument number:
>>> with Stub(Collaborator) as stub:
... stub.hello(1).returns(2) # interface mismatch exception
Traceback (most recent call last):
...
TypeError: Collaborator.hello() takes exactly 1 argument (2 given)
“free” Stub¶
This allows you to invoke any method you want because it is not restricted to an interface.
from doublex import Stub, assert_that, is_
# given
with Stub() as stub:
stub.foo('hi').returns(10)
# when
result = stub.foo('hi')
# then
assert_that(result, is_(10))
Spy¶
Hint: Spies remember everything that happens to them.
Spy extends the Stub functionality allowing you to assert on the invocation it receives since its creation.
Invocations over the Spy must meet the collaborator interface.
from hamcrest import contains_string
from doublex import Spy, assert_that, called
class Sender:
def say(self):
return "hi"
def send_mail(self, address, force=True):
pass # [some amazing code]
sender = Spy(Sender)
sender.send_mail("john.doe@example.net") # right, Sender.send_mail interface support this
assert_that(sender.send_mail, called())
assert_that(sender.send_mail, called().with_args("john.doe@example.net"))
assert_that(sender.send_mail, called().with_args(contains_string("@example.net")))
sender.bar() # interface mismatch exception
Traceback (most recent call last):
...
AttributeError: 'Sender' object has no attribute 'bar'
>>> sender = Spy(Sender)
>>> sender.send_mail()
Traceback (most recent call last):
...
TypeError: Sender.send_mail() takes at least 2 arguments (1 given)
>>> sender = Spy(Sender)
>>> sender.send_mail(wrong=1)
Traceback (most recent call last):
...
TypeError: Sender.send_mail() got an unexpected keyword argument 'wrong'
>>> sender = Spy(Sender)
>>> sender.send_mail('foo', wrong=1)
Traceback (most recent call last):
...
TypeError: Sender.send_mail() got an unexpected keyword argument 'wrong'
“free” Spy¶
As the “free” Stub, this is a spy not restricted by a collaborator interface.
from doublex import Stub, assert_that
# given
with Spy() as sender:
sender.helo().returns("OK")
# when
sender.send_mail('hi')
sender.send_mail('foo@bar.net')
# then
assert_that(sender.helo(), is_("OK"))
assert_that(sender.send_mail, called())
assert_that(sender.send_mail, called().times(2))
assert_that(sender.send_mail, called().with_args('foo@bar.net'))
ProxySpy¶
Hint: Proxy spies forward invocations to its actual instance.
The ProxySpy
extends the Spy
invoking the actual instance when the corresponding
spy method is called
Warning
Note the ProxySpy
breaks isolation. It is not really a double. Therefore is always the worst double and the
last resource.
from doublex import ProxySpy, assert_that
sender = ProxySpy(Sender()) # NOTE: It takes an instance (not class)
assert_that(sender.say(), is_("hi"))
assert_that(sender.say, called())
sender.say('boo!') # interface mismatch exception
Traceback (most recent call last):
...
TypeError: Sender.say() takes exactly 1 argument (2 given)
Mock¶
Hint: Mock forces the predefined script.
Mock objects may be programmed with a sequence of method calls. Later, the double must receive exactly the same sequence of invocations (including argument values). If the sequence does not match, an AssertionError is raised. “free” mocks are provided too:
from doublex import Mock, assert_that, verify
with Mock() as smtp:
smtp.helo()
smtp.mail(ANY_ARG)
smtp.rcpt("bill@apple.com")
smtp.data(ANY_ARG).returns(True).times(2)
smtp.helo()
smtp.mail("poormen@home.net")
smtp.rcpt("bill@apple.com")
smtp.data("somebody there?")
smtp.data("I am afraid..")
assert_that(smtp, verify())
verify()
asserts invocation order. If your test does not require strict invocation
order just use any_order_verify()
matcher instead:
from doublex import Mock, assert_that, any_order_verify
with Mock() as mock:
mock.foo()
mock.bar()
mock.bar()
mock.foo()
assert_that(mock, any_order_verify())
Programmed invocation sequence also may specify stubbed return values:
from doublex import Mock, assert_that
with Mock() as mock:
mock.foo().returns(10)
assert_that(mock.foo(), is_(10))
assert_that(mock, verify())