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

Uploaded Source

File details

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

File metadata

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

File hashes

Hashes for doublex-0.2.tar.gz
Algorithm Hash digest
SHA256 e8376422235898413b6911426f19f9b08d18137de81f7d23d18cf62fdaefe27f
MD5 8859ca4de6e3ec4f7429ccbc143813d6
BLAKE2b-256 c1b342d2f6e0a3db38f142fd7ea17a21a24f05a82eadcacc0e3b71d3351df32c

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