Skip to main content

Test doubles framework for Python

Project description

A powerful test doubles framework for Python.

This started as a try to improve and simplify pyDoubles codebase and API

Source repository is: https://bitbucket.org/DavidVilla/python-doublex

Design principles

  • Doubles have not public api specific methods. It avoid silent misspelling.

  • non-proxified doubles does not require collaborator instances, they may use classes

  • hamcrest.assert_that used for all assertions

  • Mock invocation order is required by default

  • Compatible with old and new style classes

Doubles

“free” Stub

# given
stub = Stub()
with stub:
    stub.foo('hi').returns(10)
    stub.hello(ANY_ARG).returns(False)
    stub.bye().raises(SomeException)

# when
result = stub.foo()

# then
assert_that(result, is_(10))

“verified” Stub

class Collaborator:
    def hello(self):
        return "hello"

with Stub(Collaborator) as stub
    stub.hello().raises(SomeException)
    stub.foo().returns(True)  # interface mismatch exception
    stub.hello(1).returns(2)  # interface mismatch exception

“free” Spy

# 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('foo@bar.net'))

“verified” Spy

class Sender:
    def say(self):
        return "hi"

    def send_mail(self, address, force=True):
        [some amazing code]

sender = Spy(Sender)

sender.bar()        # interface mismatch exception
sender.send_mail()  # interface mismatch exception
sender.send_mail(wrong=1)         # interface mismatch exception
sender.send_mail('foo', wrong=1)  # interface mismatch exception

ProxySpy

sender = ProxySpy(Sender())  # NOTE this always takes an instance

sender.say('boo!')  # interface mismatch exception

assert_that(sender.say(), is_("hi"))
assert_that(sender.say, called())

“free” Mock

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() assert invocation order. If your test does not require strict invocation order just use any_order_verify() matcher instead:

with Mock() as mock:
    mock.foo()
    mock.bar()

mock.bar()
mock.foo()

assert_that(mock, any_order_verify())

“verified” Mock

class SMTP:
    def helo(self):
        [...]
    def mail(self, address):
        [...]
    def rcpt(self, address):
        [...]

with Mock(STMP) as smtp:
    smtp.wrong()  # interface mismatch exception
    smtp.mail()   # interface mismatch exception

stub methods

collaborator = Collaborator()
collaborator.foo = method_returning("bye")
assertEquals("bye", self.collaborator.foo())

collaborator.foo = method_raising(SomeException)
collaborator.foo()  # raises SomeException

properties

Doublex support stub and spy properties in a pretty easy way compared with other frameworks like python-mock:

class Collaborator(object):
    @property
    def prop(self):
        return 1

    @prop.setter
    def prop(self, value):
        pass

with Spy(Collaborator) as spy:
    spy.prop = 2  # stubbing its value

assert_that(spy.prop, is_(2))  # property getter invoked
assert_that(spy, property_got('prop'))

spy.prop = 4  # 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(8)))

To make property doubles is required to:

  • You must Use “verified” doubles, ie: specify a collaborator in constructor.

  • collaborator musy be new-style classes.

doublex matchers

called

called() matches any invocation to a method:

spy.Spy()
spy.m1()
spy.m2(None)
spy.m3("hi", 3.0)
spy.m4([1, 2])

assert_that(spy.m1, called())
assert_that(spy.m2, called())
assert_that(spy.m3, called())
assert_that(spy.m4, called())

never

assert_that(spy.m5, is_not(called()))  # is_not() is a hamcrest matcher
assert_that(spy.m5, never(called()))   # recommended (better report message)

called_with

called_with() matches explicit values and hamcrest matchers:

spy.Spy()

spy.m1()
spy.m2(None)
spy.m3(2)
spy.m4("hi", 3.0)
spy.m5([1, 2])
spy.m6(name="john doe")

assert_that(spy.m1, called())
assert_that(spy.m2, called())

assert_that(spy.m1, called_with())
assert_that(spy.m2, called_with(None))
assert_that(spy.m3, called_with(2))
assert_that(spy.m4, called_with("hi", 3.0))
assert_that(spy.m5, called_with([1, 2]))
assert_that(spy.m6, called_with(name="john doe"))

assert_that(spy.m3, called_with(less_than(3)))
assert_that(spy.m3, called_with(greater_than(1)))
assert_that(spy.m6, called_with(name=contains_string("doe")))

ANY_ARG

ANY_ARG is a special value that matches any value and any amount of values, including no args. For example:

spy.arg0()
spy.arg1(1)
spy.arg3(1, 2, 3)
spy.arg_karg(1, key1='a')

assert_that(spy.arg0, called_with(ANY_ARG))
assert_that(spy.arg1, called_with(ANY_ARG))
assert_that(spy.arg3, called_with(1, ANY_ARG))
assert_that(spy.arg_karg, called_with(1, ANY_ARG))

Also for stubs:

with Stub() as stub:
    stub.foo(ANY_ARG).returns(True)
    stub.bar(1, ANY_ARG).returns(True)

assert_that(stub.foo(), is_(True))
assert_that(stub.foo(1), is_(True))
assert_that(stub.foo(key1='a'), is_(True))
assert_that(stub.foo(1, 2, 3, key1='a', key2='b'), is_(True))

assert_that(stub.foo(1, 2, 3), is_(True))
assert_that(stub.foo(1, key1='a'), is_(True))

But, if you want match any single value, use hamcrest matcher anything():

spy.foo(1, 2, 3)
assert_that(spy.foo, called_with(1, anything(), 3))

spy.bar(1, key=2)
assert_that(spy.bar, called_with(1, key=anything()))

matchers, matchers, hamcrest matchers…

doublex support all hamcrest matchers, and their amazing combinations.

checking spied calling args

spy = Spy()
spy.foo("abcd")

assert_that(spy.foo, called_with(has_length(4)))
assert_that(spy.foo, called_with(has_length(greater_than(3))))
assert_that(spy.foo, called_with(has_length(less_than(5))))
assert_that(spy.foo, is_not(called_with(has_length(greater_than(5)))))

stubbing

with Spy() as spy:
    spy.foo(has_length(less_than(4))).returns('<4')
    spy.foo(has_length(4)).returns('four')
    spy.foo(has_length(
               all_of(greater_than(4),
                      less_than(8)))).returns('4<x<8')
    spy.foo(has_length(greater_than(8))).returns('>8')

assert_that(spy.foo((1, 2)), is_('<4'))
assert_that(spy.foo('abcd'), is_('four'))
assert_that(spy.foo('abcde'), is_('4<x<8'))
assert_that(spy.foo([0] * 9), is_('>8'))

checking invocation ‘times’

spy.foo()
spy.foo(1)
spy.foo(1)
spy.foo(2)

assert_that(spy.never, never(called()))                      # = 0 times
assert_that(spy.foo, called())                               # > 0
assert_that(spy.foo, called().times(greater_than(0)))        # > 0 (same)
assert_that(spy.foo, called().times(4))                      # = 4
assert_that(spy.foo, called().times(greater_than(2)))        # > 2
assert_that(spy.foo, called().times(less_than(6)))           # < 6

assert_that(spy.foo, is_not(called_with(5)))                 # = 0 times
assert_that(spy.foo, called_with().times(1))                 # = 1
assert_that(spy.foo, called_with(anything()))                # > 0
assert_that(spy.foo, called_with(anything()).times(4))       # = 4
assert_that(spy.foo, called_with(1).times(2))                # = 2
assert_that(spy.foo, called_with(1).times(greater_than(1)))  # > 1
assert_that(spy.foo, called_with(1).times(less_than(5)))     # < 5

Stub observers

Stub observers allow you to execute extra code (similar to python-mock “side effects”):

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

Stub delegates

The value returned by the stub may be delegated to function, method or other callable…:

def get_user():
    return "Freddy"

with Stub() as stub:
    stub.user().delegates(get_user)
    stub.foo().delegates(lambda: "hello")

assert_that(stub.user(), is_("Freddy"))
assert_that(stub.foo(), is_("hello"))

It may be delegated to iterables or generators too!:

with Stub() as stub:
    stub.foo().delegates([1, 2, 3])

assert_that(stub.foo(), is_(1))
assert_that(stub.foo(), is_(2))
assert_that(stub.foo(), is_(3))

Mimic doubles

Usually double instances behave as collaborator subrogates, but they do not expose the same class hierarchy, and usually this is pretty enough when the code uses “duck typing”:

class A(object):
    pass

class B(A):
    pass

>>> spy = Spy(B())
>>> isinstance(spy, Spy)
True
>>> isinstance(spy, B)
False

But some third party library DOES strict type checking using isinstance() invalidating our doubles. For these cases you can use Mimic’s. Mimic class can decorate any double class to achive full replacement (Liskov principle):

>>> spy = Mimic(Spy, B)
>>> isinstance(spy, B)
True
>>> isinstance(spy, A)
True
>>> isinstance(spy, Spy)
True
>>> isinstance(spy, Stub)
True
>>> isinstance(spy, object)
True

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

doublex-0.5.tar.gz (13.4 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page