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

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

assert_that(spy.m2, never(called_with()))
assert_that(spy.m2, never(called_with(3)))

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.3.tar.gz (11.8 kB view details)

Uploaded Source

File details

Details for the file doublex-0.3.tar.gz.

File metadata

  • Download URL: doublex-0.3.tar.gz
  • Upload date:
  • Size: 11.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No

File hashes

Hashes for doublex-0.3.tar.gz
Algorithm Hash digest
SHA256 3c8ab018e5ab91c853d0e00346e2973626abcbbea31f6077e9411cfe9d1fdf37
MD5 099db0ae0c2292f0ac728baefc80efc2
BLAKE2b-256 5c71af5ea5d06261e7086a89cc521de0707c28e3a18cf7b04f422214869bc5fb

See more details on using hashes here.

Supported by

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