Author: @david_vi11a
Description: doublex: Python test doubles framework

# doublex

#### Python test doubles framework

@david_vi11a

what are doubles?

Test doubles are atrezzo objects.

why should we use doubles?

Unit tests must isolate the SUT.

So all collaborators should be replaced.

They require (and promote) your classes meets DIP and LSP.

then, instead of production class instances...

     collaborator = Collaborator()

     sut = SUT(collaborator)

     sut.exercise()

...give it doubles

     double = Double(Collaborator)

     sut = SUT(double)

     sut.exercise()

## which is the right double?

- Dummy: a placeholder object, never invoked
- Fake: Q&D replacement, not suitable for production
- Stub: returns hardcoded responses: It say you want to hear
- Spy: Stub that records received invocations
- Mock: holds and check programmed expectations

[ according to xunitpatterns.com/Test Double.html ]

## EXAMPLE
account service

      class AccountService:
          def __init__(self, store, password_service):
    	  [...]

          def create_user(self, login):
              [...]
              - raise InvalidPassword()
              - raise AlreadyExists()

          def create_group(self, group_name, user_names):
              [...]

[ slides related to that example
have gray background ]

## account service

collaborators

      class AccountStore:
          def save(self, login, password):
              [...]

          def has_user(self, login):
              [...]

      class PasswordService:
          def generate(self):
              [...]

## Stub

In a free stub, any method may be invoked:

      class AccountTests(TestCase):
          def test_account_creation(self):
              with Stub() as password_service:
                  password_service.generate().returns('secret')

              service = AccountService(store=Stub(), password_service)

              service.create_user('John')

## Stub

... you can set return value depending on arguments

     with Stub() as stub:
         stub.foo(2, 2).returns(100)
         stub.foo(3, ANY_ARG).returns(200)

     assert_that(stub.foo(1, 1), is_(None))
     assert_that(stub.foo(2, 2), is_(100))
     assert_that(stub.foo(3, 0), is_(200))

## Stub

... or by hamcrest matcher

     with Stub() as stub:
         stub.foo(2, greater_than(4)).returns(100)

     assert_that(stub.foo(2, 1), is_(None))
     assert_that(stub.foo(2, 5), is_(100))

## Stub

... or by composite hamcrest matcher

     with Stub() as stub:
         stub.foo(2, has_length(all_of(
             greater_than(4), less_than(8)))).returns(1000)

     assert_that(stub.foo(2, "bad"), is_(None))
     assert_that(stub.foo(2, "enough"), is_(1000))

## Stub

interface may be restricted to a given class:

     with Stub(PasswordService) as password_service:
         password_service.generate().returns('secret')

     stub.generate()
     stub.generate(9)
    TypeError: PasswordService.generate() takes exactly 1 argument (2 given)

     stub.wrong()
    AttributeError: 'PasswordService' object has no attribute 'wrong'

## in our AccountService test:

      class AccountTests(TestCase):
          def test_account_creation(self):
              with Stub(PasswordService) as password_service:
                  password_service.generate().returns('secret')

              service = AccountService(store=Stub(), password_service)

              service.create_user('John')

... is 'store' really called??

we need a spy

## Spy

checking double invocations: called()

     store = Spy(AccountStore)
     service = AccountService(store, password_service)

     service.create_group('team', ['John', 'Peter', 'Alice'])

     assert_that(store.save, called())

but... is really called three times?

## Spy

checking called times: times()

(also with matchers)

     store = Spy(AccountStore)
     service = AccountService(store, password_service)

     service.create_group('team', ['John', 'Peter', 'Alice'])

     assert_that(store.save, called().times(3))
     assert_that(store.save, called().times(greater_than(2)))

but... is really called with the right arguments?

## Spy

check argument values: with_args()

(also with matchers)

     store = Spy(AccountStore)
     service = AccountService(store, password_service)

     service.create_user('John')

     assert_that(store.save, called().with_args('John', 'secret'))
     assert_that(store.save, called().with_args('John', ANY_ARG))
     assert_that(store.save,
                 called().with_args(contains_string('oh'), ANY_ARG))
     assert_that(store.save,
                 never(called().with_args('Alice', anything())))

## Spy

check keyword argument

(also with matcher)

     spy = Spy()
     spy.foo(name="Mary")

     assert_that(spy.foo,
                 called().with_args(name="Mary"))

     assert_that(spy.foo,
                 called().with_args(name=contains_string("ar")))

## Spy

meaning-full report messages!

     service.create_group('team', ['John', 'Alice'])

     assert_that(store.save, called().with_args('Peter'))
    AssertionError:
    Expected: these calls:
              AccountStore.save('Peter')
         but: calls that actually ocurred were:
              AccountStore.has_user('John')
              AccountStore.save('John', 'secret')
              AccountStore.has_user('Alice')
              AccountStore.save('Alice', 'secret')

## ProxySpy

propagates invocations to the collaborator

     with ProxySpy(AccountStore()) as store:
         store.has_user('John').returns(True)

     service = AccountService(store, password_service)

     with self.assertRaises(AlreadyExists):
         service.create_user('John')

CAUTION: ProxySpy is not a true double,
this invokes the actual `AccountStore` instance!

## Mock

programming expectations

            with Mock(AccountStore) as store:
                store.has_user('John')
                store.save('John', anything())
                store.has_user('Peter')
                store.save('Peter', anything())

            service = AccountService(store, password_service)

    	service.create_group('team', ['John', 'Peter'])

            assert_that(store, verify())

Mock assures these invocations (and only these) are ocurred.

## ah hoc stub methods

     collaborator = Collaborator()
     collaborator.foo = method_returning('bye')
     assert_that(self.collaborator.foo(), is_('bye'))

     collaborator.foo = method_raising(SomeException)
     collaborator.foo()
    SomeException:

## stub observers

attach additional behavior

     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

delegating to callables

     def get_pass():
         return "12345"

     with Stub(PasswordService) as password_service:
         password_service.generate().delegates(get_pass)

     store = Spy(AccountStore)
     service = AccountService(store, password_service)

     service.create_user('John')

     assert_that(store.save, called().with_args('John', '12345'))

## stub delegates

delegating to iterables/generators

     with Stub(PasswordService) as password_service:
         password_service.generate().delegates(["12345", "mypass", "nope"])

     store = Spy(AccountStore)
     service = AccountService(store, password_service)

     service.create_group('team', ['John', 'Peter', 'Alice'])

     assert_that(store.save, called().with_args('John', '12345'))
     assert_that(store.save, called().with_args('Peter', 'mypass'))
     assert_that(store.save, called().with_args('Alice', 'nope'))

## stubbing properties

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

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

     with Spy(Collaborator) as spy:
         spy.prop = 2

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

## spying properties

(also with matcher)

     assert_that(spy, property_got('prop'))

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

## Questions?

## References

- python-doublex
- pydoubles.org