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

“verified” Stub

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

with Stub(Collaborator) as stub
    stub.hello().raises(SomeException)
    stub.foo().returns(True)  # raises ApiMismatch exception
    stub.hello(1).returns(2)  # raises ApiMismatch 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(), "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()        # raises ApiMismatch exception
sender.send_mail()  # raises ApiMismatch exception
sender.send_mail(wrong=1)         # raises ApiMismatch exception
sender.send_mail('foo', wrong=1)  # raises ApiMismatch exception

ProxySpy

sender = Spy(Sender())  # must give an instance

sender.say('boo!')  # raises ApiMismatch exception

assert_that(sender.say(), "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, meets_expectations())

“verified” Mock

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

with Mock(STMP) as smtp:
    smtp.wrong()  # raises ApiMismatch exception
    smtp.mail()   # raises ApiMismatch exception

stub methods

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

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

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

called_with

called_with() matches specific arguments:

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

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

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, is_not(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…:

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

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

It may be delegated to iterators 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))

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.2.tar.gz (9.9 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