8'))
def test_stub_contains_string(self):
with Stub() as stub:
stub.method(contains_string("some")).returns(1000)
assert_that(stub.method("awesome"), is_(1000))
# doc FIXME: assure this is tested with doctests and remove
def test_times_arg_may_be_matcher(self):
self.spy.foo()
self.spy.foo(1)
self.spy.foo(1)
self.spy.foo(2)
assert_that(self.spy.never, is_not(called())) # = 0 times
assert_that(self.spy.foo, called()) # > 0
assert_that(self.spy.foo, called().times(greater_than(0))) # > 0 (same)
assert_that(self.spy.foo, called().times(4)) # = 4
assert_that(self.spy.foo, called().times(greater_than(2))) # > 2
assert_that(self.spy.foo, called().times(less_than(6))) # < 6
assert_that(self.spy.foo, is_not(called().with_args(5))) # = 0 times
assert_that(self.spy.foo, called().with_args().times(1)) # = 1
assert_that(self.spy.foo, called().with_args(anything())) # > 0
assert_that(self.spy.foo, called().with_args(ANY_ARG).times(4)) # = 4
assert_that(self.spy.foo, called().with_args(1).times(2)) # = 2
assert_that(self.spy.foo, called().with_args(1).times(greater_than(1))) # > 1
assert_that(self.spy.foo, called().with_args(1).times(less_than(5))) # < 5
# doc FIXME: assure this is tested with doctests and remove
def test_called_args(self):
self.spy.m1()
self.spy.m2(None)
self.spy.m3(2)
self.spy.m4("hi", 3.0)
self.spy.m5([1, 2])
self.spy.m6(name="john doe")
assert_that(self.spy.m1, called())
assert_that(self.spy.m2, called())
assert_that(self.spy.m1, called().with_args())
assert_that(self.spy.m2, called().with_args(None))
assert_that(self.spy.m3, called().with_args(2))
assert_that(self.spy.m4, called().with_args("hi", 3.0))
assert_that(self.spy.m5, called().with_args([1, 2]))
assert_that(self.spy.m6, called().with_args(name="john doe"))
assert_that(self.spy.m3, called().with_args(less_than(3)))
assert_that(self.spy.m3, called().with_args(greater_than(1)))
assert_that(self.spy.m6, called().with_args(name=contains_string("doe")))
# new on 1.7
def test_assert_that_requires_a_matcher(self):
self.assertRaises(MatcherRequiredError, assert_that, self.spy.m1, True)
# from pydoubles docs
def test_has_entry_matcher(self):
with Spy() as spy:
spy.one_arg_method(has_entry(is_('two'), 2)).returns(1000)
assert_that(spy.one_arg_method({'one': 1, 'two': 2}), is_(1000))
def test_all_of_matcher(self):
with Spy() as spy:
spy.one_arg_method(all_of(starts_with('h'), instance_of(str))).returns(1000)
assert_that(spy.one_arg_method('hello'), is_(1000))
class StubObserverTests(TestCase):
def setUp(self):
self.stub = Stub()
def test_observer_called(self):
observer = Observer()
self.stub.foo.attach(observer.update)
self.stub.foo(2)
assert_that(observer.state, is_(2))
def test_observer_called_tested_using_a_doublex_spy(self):
observer = Spy()
self.stub.foo.attach(observer.update)
self.stub.foo(2)
assert_that(observer.update, called().with_args(2))
class StubDelegateTests(TestCase):
def setUp(self):
self.stub = Stub()
def assert_012(self, method):
for x in range(3):
assert_that(method(), is_(x))
def test_delegate_to_other_method(self):
with self.stub:
self.stub.foo().delegates(Collaborator().hello)
assert_that(self.stub.foo(), is_("hello"))
def test_delegate_to_list(self):
with self.stub:
self.stub.foo().delegates(list(range(3)))
self.assert_012(self.stub.foo)
def test_delegate_to_generator(self):
with self.stub:
self.stub.foo().delegates(x for x in range(3))
self.assert_012(self.stub.foo)
def test_delegate_to_count(self):
with self.stub:
self.stub.foo().delegates(itertools.count())
self.assert_012(self.stub.foo)
def test_delegate_to_lambda(self):
with self.stub:
self.stub.foo().delegates(lambda: 2)
assert_that(self.stub.foo(), is_(2))
def test_delegate_to_another_stub(self):
stub2 = Stub()
with stub2:
stub2.bar().returns("hi!")
with self.stub:
self.stub.foo().delegates(stub2.bar)
assert_that(self.stub.foo(), is_("hi!"))
def test_not_delegable_object(self):
try:
with self.stub:
self.stub.foo().delegates(None)
self.fail("Exception should be raised")
except WrongApiUsage as e:
expected = "delegates() must be called with callable or iterable instance (got 'None' instead)"
assert_that(str(e), contains_string(expected))
# FIXME: explain in docs
def test_delegates_with_params(self):
with self.stub:
self.stub.foo(anything()).delegates(lambda x: x + 2)
assert_that(self.stub.foo(3), is_(5))
# new on 1.8.2
# FIXME: include in docs
def test_delegate_to_dict(self):
with self.stub:
self.stub.foo(anything()).delegates({0: 2, 1: 7, 2: 12})
assert_that(self.stub.foo(1), is_(7))
class MockDelegateTest(TestCase):
def setUp(self):
self.mock = Mock()
def assert_012(self, method):
for x in range(3):
assert_that(method(), is_(x))
def test_delegate_to_list_is_only_an_expectation(self):
with self.mock:
self.mock.foo().delegates(list(range(3)))
self.mock.foo()
assert_that(self.mock, verify())
class MimicTests(TestCase):
class A(object):
def method_a(self, n):
return n + 1
class B(A):
def method_b(self):
return "hi"
def test_normal_spy_does_not_inherit_collaborator_superclasses(self):
spy = Spy(self.B)
assert_that(not isinstance(spy, self.B))
def test_mimic_spy_DOES_inherit_collaborator_superclasses(self):
spy = Mimic(Spy, self.B)
for cls in [self.B, self.A, Spy, Stub, object]:
assert_that(spy, instance_of(cls))
def test_mimic_stub_works(self):
stub = Mimic(Stub, self.B)
with stub:
stub.method_a(2).returns(3)
assert_that(stub.method_a(2), is_(3))
def test_mimic_stub_from_instance(self):
stub = Mimic(Stub, self.B())
with stub:
stub.method_a(2).returns(3)
assert_that(stub.method_a(2), is_(3))
def test_mimic_spy_works(self):
spy = Mimic(Spy, self.B)
with spy:
spy.method_a(5).returns(True)
assert_that(spy.method_a(5), is_(True))
assert_that(spy.method_a, called())
assert_that(spy.method_a, called().with_args(5))
def test_mimic_proxy_spy_works(self):
spy = Mimic(ProxySpy, self.B())
assert_that(spy.method_a(5), is_(6))
assert_that(spy.method_a, called())
assert_that(spy.method_a, called().with_args(5))
def test_mimic_mock_works(self):
mock = Mimic(Mock, self.B)
with mock:
mock.method_a(2)
mock.method_a(2)
assert_that(mock, verify())
class PropertyTests(TestCase):
def test_stub_notset_property_is_None(self):
stub = Stub(ObjCollaborator)
assert_that(stub.prop, is_(None))
def test_stub_property(self):
stub = Stub(ObjCollaborator)
with stub:
stub.prop = 2
assert_that(stub.prop, is_(2))
def test_spy_get_property_using_class(self):
spy = Spy(ObjCollaborator)
skip = spy.prop
assert_that(spy, property_got('prop'))
def test_spy_get_property_using_instance(self):
spy = Spy(ObjCollaborator())
skip = spy.prop
assert_that(spy, property_got('prop'))
def test_spy_not_get_property(self):
spy = Spy(ObjCollaborator)
assert_that(spy, never(property_got('prop')))
def test_spy_get_property_fail(self):
spy = Spy(ObjCollaborator)
with self.assertRaises(AssertionError):
assert_that(spy, property_got('prop'))
def test_spy_set_property_using_class(self):
spy = Spy(ObjCollaborator)
spy.prop = 2
assert_that(spy, property_set('prop'))
def test_spy_set_property_using_instance(self):
spy = Spy(ObjCollaborator())
spy.prop = 2
assert_that(spy, property_set('prop'))
def test_spy_not_set_property(self):
spy = Spy(ObjCollaborator)
assert_that(spy, never(property_set('prop')))
def test_spy_set_property_fail(self):
spy = Spy(ObjCollaborator)
with self.assertRaises(AssertionError):
assert_that(spy, property_set('prop'))
def test_spy_set_property_to(self):
spy = Spy(ObjCollaborator)
spy.prop = 2
assert_that(spy, property_set('prop').to(2))
assert_that(spy, never(property_set('prop').to(5)))
def test_spy_set_property_times(self):
spy = Spy(ObjCollaborator)
spy.prop = 2
spy.prop = 3
assert_that(spy, property_set('prop').to(2))
assert_that(spy, property_set('prop').to(3))
assert_that(spy, property_set('prop').times(2))
def test_spy_set_property_to_times(self):
spy = Spy(ObjCollaborator)
spy.prop = 3
spy.prop = 3
assert_that(spy, property_set('prop').to(3).times(2))
def test_properties_are_NOT_shared_among_doubles(self):
stub1 = Stub(ObjCollaborator)
stub2 = Stub(ObjCollaborator)
stub1.prop = 1000
assert_that(stub2.prop, is_not(1000))
assert_that(stub1.__class__ is not stub2.__class__)
def test_spy_get_readonly_property_with_deco(self):
spy = Spy(ObjCollaborator)
skip = spy.prop_deco_readonly
assert_that(spy, property_got('prop_deco_readonly'))
def test_spy_SET_readonly_property_with_deco(self):
spy = Spy(ObjCollaborator)
try:
spy.prop_deco_readonly = 'wrong'
self.fail('should raise exception')
except AttributeError:
pass
def test_hamcrest_matchers(self):
spy = Spy(ObjCollaborator)
spy.prop = 2
spy.prop = 3
assert_that(spy,
property_set('prop').to(greater_than(1)).
times(less_than(3)))
def test_proxyspy_get_actual_property(self):
collaborator = ObjCollaborator()
sut = ProxySpy(collaborator)
assert_that(sut.prop, is_(1))
def test_proxyspy_get_stubbed_property(self):
collaborator = ObjCollaborator()
with ProxySpy(collaborator) as sut:
sut.prop = 2
assert_that(sut.prop, is_(2))
def test_proxyspy_set_property(self):
collaborator = ObjCollaborator()
sut = ProxySpy(collaborator)
sut.prop = 20
assert_that(sut.prop, is_(20))
assert_that(collaborator.prop, is_(20))
# new on 1.8
class PropertyMockTests(TestCase):
def test_mock_get(self):
with Mock(ObjCollaborator) as mock:
mock.prop
mock.prop
assert_that(mock, verify())
def test_mock_get_but_never_got(self):
with Mock(ObjCollaborator) as mock:
mock.prop
with self.assertRaises(AssertionError):
assert_that(mock, verify())
def test_mock_get_too_many_times(self):
with Mock(ObjCollaborator) as mock:
mock.prop
mock.prop
mock.prop
with self.assertRaises(AssertionError):
assert_that(mock, verify())
def test_mock_set(self):
with Mock(ObjCollaborator) as mock:
mock.prop = 5
mock.prop = 5
assert_that(mock, verify())
def test_mock_set_but_never_set(self):
with Mock(ObjCollaborator) as mock:
mock.prop = 5
with self.assertRaises(AssertionError):
assert_that(mock, verify())
def test_mock_set_too_many_times(self):
with Mock(ObjCollaborator) as mock:
mock.prop = 5
mock.prop = 5
mock.prop = 5
with self.assertRaises(AssertionError):
assert_that(mock, verify())
def test_mock_set_wrong_value(self):
with Mock(ObjCollaborator) as mock:
mock.prop = 5
with self.assertRaises(AssertionError):
mock.prop = 8
def test_mock_set_anything(self):
with Mock(ObjCollaborator) as mock:
mock.prop = anything()
mock.prop = 5
assert_that(mock, verify())
def test_mock_set_matcher(self):
with Mock(ObjCollaborator) as mock:
mock.prop = all_of(greater_than(8), less_than(12))
mock.prop = 10
assert_that(mock, verify())
class AsyncTests(TestCase):
class SUT(object):
def __init__(self, collaborator):
self.collaborator = collaborator
def send_data(self, data=0):
thread.start_new_thread(self.collaborator.write, (data,))
def test_spy_call_without_async_feature(self):
# given
barrier = threading.Event()
with Spy() as spy:
spy.write.attach(lambda *args: barrier.set)
sut = AsyncTests.SUT(spy)
# when
sut.send_data()
barrier.wait(1) # test probably FAILS without this
# then
assert_that(spy.write, called())
def test_spy_call_with_async_feature(self):
# given
spy = Spy()
sut = AsyncTests.SUT(spy)
# when
sut.send_data()
# then
assert_that(spy.write, called().async_mode(timeout=1))
def test_spy_async_support_1_call_only(self):
# given
spy = Spy()
sut = AsyncTests.SUT(spy)
# when
sut.send_data(3)
sut.send_data(3)
# then
with self.assertRaises(WrongApiUsage):
assert_that(spy.write, called().async_mode(timeout=1).with_args(3).times(2))
def test_spy_async_stubbed(self):
# given
with Spy() as spy:
spy.write(ANY_ARG).returns(100)
sut = AsyncTests.SUT(spy)
# when
sut.send_data(3)
# then
assert_that(spy.write, called().async_mode(timeout=1))
# new on 1.7
class with_some_args_matcher_tests(TestCase):
def test_one_arg(self):
spy = Spy(Collaborator)
spy.mixed_method(5)
assert_that(spy.mixed_method, called().with_args(5))
assert_that(spy.mixed_method, called().with_args(arg1=5))
def test_two_arg(self):
spy = Spy(Collaborator)
spy.two_args_method(5, 10)
assert_that(spy.two_args_method, called().with_args(5, 10))
assert_that(spy.two_args_method, called().with_args(arg1=5, arg2=10))
assert_that(spy.two_args_method, called().with_some_args(arg1=5))
assert_that(spy.two_args_method, called().with_some_args(arg2=10))
assert_that(spy.two_args_method, called().with_some_args())
def test_free_spy(self):
spy = Spy()
spy.foo(1, 3)
with self.assertRaises(WrongApiUsage):
assert_that(spy.foo, called().with_some_args())
# new on 1.7
class Stub_default_behavior_tests(TestCase):
def test_set_return_globally(self):
StubClone = Stub._clone_class()
set_default_behavior(StubClone, method_returning(20))
stub = StubClone()
assert_that(stub.unknown(), is_(20))
def test_set_exception_globally(self):
StubClone = Stub._clone_class()
set_default_behavior(StubClone, method_raising(SomeException))
stub = StubClone()
with self.assertRaises(SomeException):
stub.unknown()
def test_set_return_by_instance(self):
stub = Stub()
set_default_behavior(stub, method_returning(20))
assert_that(stub.unknown(), is_(20))
def test_set_exception_by_instance(self):
stub = Stub()
set_default_behavior(stub, method_raising(SomeException))
with self.assertRaises(SomeException):
stub.unknown()
def test_restricted_stub(self):
stub = Stub(Collaborator)
set_default_behavior(stub, method_returning(30))
with stub:
stub.hello().returns(1000)
assert_that(stub.something(), is_(30))
assert_that(stub.hello(), is_(1000))
# new on 1.7
class Spy_default_behavior_tests(TestCase):
def test_set_return_globally(self):
SpyClone = Spy._clone_class()
set_default_behavior(SpyClone, method_returning(20))
spy = SpyClone()
assert_that(spy.unknown(7), is_(20))
assert_that(spy.unknown, called().with_args(7))
assert_that(spy.unknown, never(called().with_args(9)))
def test_set_return_by_instance(self):
spy = Spy()
set_default_behavior(spy, method_returning(20))
assert_that(spy.unknown(7), is_(20))
assert_that(spy.unknown, called().with_args(7))
# new on 1.7
class ProxySpy_default_behavior_tests(TestCase):
def test_this_change_proxyspy_default_behavior(self):
spy = ProxySpy(Collaborator())
assert_that(spy.hello(), is_("hello"))
set_default_behavior(spy, method_returning(40))
assert_that(spy.hello(), is_(40))
class orphan_spy_method_tests(TestCase):
# new on 1.8.2
# issue 21
def test_spy(self):
m = method_returning(3)
m()
assert_that(m, called())
# FIXME: new on tip
class new_style_orphan_methods_tests(TestCase):
def setUp(self):
self.obj = Collaborator()
def test_stub_method(self):
with Stub() as stub:
stub.method(1).returns(100)
stub.method(2).returns(200)
self.obj.foo = stub.method
assert_that(self.obj.foo(0), is_(None))
assert_that(self.obj.foo(1), is_(100))
assert_that(self.obj.foo(2), is_(200))
def test_spy_method(self):
with Spy() as spy:
spy.method(1).returns(100)
spy.method(2).returns(200)
spy.method(3).raises(SomeException)
self.obj.foo = spy.method
assert_that(self.obj.foo(0), is_(None))
assert_that(self.obj.foo(1), is_(100))
assert_that(self.obj.foo(2), is_(200))
with self.assertRaises(SomeException):
self.obj.foo(3)
assert_that(self.obj.foo, called().times(4))
assert_that(spy.method, called().times(4))
# def test_spy_method__brief_method(self):
# with method() as self.obj.foo:
# self.obj.foo().returns(100)
# self.obj.foo(2).returns(200)
#
# assert_that(self.obj.foo(), is_(100))
# assert_that(self.obj.foo(2), is_(200))
# assert_that(self.obj.foo(3), is_(None))
#
# assert_that(self.obj.foo, called().times(3))
# assert_that(spy.method, called().times(3))
class custom_types_tests(TestCase):
# issue 22
def test_custom_equality_comparable_objects(self):
class A(object):
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
with Stub() as stub:
stub.foo(A(1)).returns(A(1))
self.assertEquals(A(1), stub.foo(A(1)))
class when_tests(TestCase):
def test_stub_when(self):
stub = Stub()
when(stub).add(2, 2).returns(5)
when(stub).add(2, 4).returns(8)
when(stub).sub(3, ANY_ARG).returns(100)
when(stub).foo(ANY_ARG).returns_input()
when(stub).foo(anything(), 7).returns(300)
assert_that(stub.add(2, 2), is_(5))
assert_that(stub.add(2, 4), is_(8))
assert_that(stub.sub(3, 600), is_(100))
assert_that(stub.foo((8, 2)), is_((8, 2)))
assert_that(stub.foo(3, 7), is_(300))
assert_that(stub.add(), is_(None))
def test_stub_when_with_args(self):
stub = Stub()
when(stub).some_calculation(5).returns(10)
when(stub).some_calculation(10).returns(20)
assert_that(stub.some_calculation(5), is_(10))
def test_spy_when(self):
spy = Spy()
when(spy).add(2, 2).returns(5)
when(spy).add(2, 4).returns(8)
assert_that(spy.add(2, 2), is_(5))
assert_that(spy.add(2, 4), is_(8))
assert_that(spy.add(), is_(None))
# from pydoubles docs
def test_has_entry_matcher(self):
spy = Spy()
when(spy).one_arg_method(has_entry(is_('two'), 2)).returns(1000)
assert_that(spy.one_arg_method({'one': 1, 'two': 2}), is_(1000))
def test_all_of_matcher(self):
spy = Spy()
when(spy).one_arg_method(all_of(starts_with('h'), instance_of(str))).returns(1000)
assert_that(spy.one_arg_method('hello'), is_(1000))
class expect_call_tests(TestCase):
def test_expect_call(self):
mock = Mock()
expect_call(mock).add(2, 4)
expect_call(mock).add(2, 2).returns(5)
mock.add(2, 4)
assert_that(mock.add(2, 2), is_(5))
assert_that(mock, verify())
def test_except_call_with_stub_or_spy_forbidden(self):
with self.assertRaises(WrongApiUsage):
expect_call(Stub()).foo()
with self.assertRaises(WrongApiUsage):
expect_call(Spy()).foo()
# new on 1.7.2
class VarArgsTest(TestCase):
def test_stub_args(self):
stub = Stub(Collaborator)
with stub:
stub.varargs(1).returns(10)
stub.varargs(1, 2).returns(200)
stub.varargs(1, 3, ANY_ARG).returns(300)
stub.varargs(2, anything()).returns(400)
assert_that(stub.varargs(42), is_(None))
assert_that(stub.varargs(1), is_(10))
assert_that(stub.varargs(1, 2), is_(200))
assert_that(stub.varargs(1, 2, 7), is_(None))
assert_that(stub.varargs(1, 3), is_(300))
assert_that(stub.varargs(1, 3, 7), is_(300))
assert_that(stub.varargs(1, 5), is_(None))
assert_that(stub.varargs(2), is_(None))
assert_that(stub.varargs(2, 3), is_(400))
assert_that(stub.varargs(2, 3, 4), is_(None))
def test_spy_args(self):
spy = Spy(Collaborator)
spy.varargs(1, 2, 3)
assert_that(spy.varargs, called())
assert_that(spy.varargs, called().with_args(1, 2, 3))
assert_that(spy.varargs, called().with_args(1, ANY_ARG))
def test_spy_kargs(self):
spy = Spy(Collaborator)
spy.varargs(one=1, two=2)
assert_that(spy.varargs, called())
assert_that(spy.varargs, called().with_args(one=1, two=2))
assert_that(spy.varargs, called().with_args(one=1, two=anything()))
def test_with_some_args_is_not_applicable(self):
spy = Spy(Collaborator)
spy.varargs(one=1, two=2)
try:
assert_that(spy.varargs, called().with_some_args(one=1))
self.fail('exception should be raised')
except WrongApiUsage as e:
assert_that(str(e),
contains_string('with_some_args() can not be applied to method Collaborator.varargs(self, *args, **kargs)'))
# new on 1.7.2
class TracerTests(TestCase):
def setUp(self):
self.out = StringIO()
self.tracer = Tracer(self.out.write)
def test_trace_single_method(self):
with Stub() as stub:
stub.foo(ANY_ARG).returns(1)
self.tracer.trace(stub.foo)
stub.foo(1, two=2)
assert_that(self.out.getvalue(), is_("Stub.foo(1, two=2)"))
def test_trace_single_non_stubbed_method(self):
stub = Stub()
self.tracer.trace(stub.non)
stub.non(1, "two")
assert_that(self.out.getvalue(), is_("Stub.non(1, 'two')"))
def test_trace_all_double_INSTANCE_methods(self):
stub = Stub()
self.tracer.trace(stub)
stub.bar(2, "three")
assert_that(self.out.getvalue(), is_("Stub.bar(2, 'three')"))
def test_trace_all_double_CLASS_methods(self):
self.tracer.trace(Stub)
stub = Stub()
stub.fuzz(3, "four")
assert_that(self.out.getvalue(), is_("Stub.fuzz(3, 'four')"))
def test_trace_get_property(self):
stub = Stub(ObjCollaborator)
self.tracer.trace(stub)
stub.prop
assert_that(self.out.getvalue(),
is_("ObjCollaborator.prop gotten"))
def test_trace_set_property(self):
stub = Stub(ObjCollaborator)
self.tracer.trace(stub)
stub.prop = 2
assert_that(self.out.getvalue(),
is_("ObjCollaborator.prop set to 2"))
# https://bitbucket.org/DavidVilla/python-doublex/issues/24/customize-different-return-values-for
class function_doubles(TestCase):
def test_hack_function_spy(self):
with Stub() as stub:
method = Method(stub, 'orphan')
method("hello").returns("world")
assert_that(method("hello"), is_("world"))
# TODO
# def test_stub_call(self):
# with Stub() as stub:
# stub("hello").returns("world")
# assert_that(stub("hello"), is_("world"))
class SomeException(Exception):
pass
class Observer(object):
def __init__(self):
self.state = None
def update(self, *args, **kargs):
self.state = args[0]
class ObjCollaborator(object):
def __init__(self):
self._propvalue = 1
def no_args(self):
return 1
def prop_getter(self):
return self._propvalue
def prop_setter(self, value):
self._propvalue = value
prop = property(prop_getter, prop_setter)
@property
def prop_deco_readonly(self):
return 2
class Collaborator:
"""
The original object we double in tests
"""
class_attr = "OK"
def __init__(self):
self.instance_attr = 300
def hello(self):
return "hello"
def something(self):
return "ok"
def one_arg_method(self, arg1):
return arg1
def two_args_method(self, arg1, arg2):
return arg1 + arg2
def three_args_method(self, arg1, arg2, arg3):
return arg1 + arg2 + arg3
def kwarg_method(self, key_param=False):
return key_param
def mixed_method(self, arg1, key_param=False):
return key_param + arg1
def void_method(self):
pass
def method_one(self, arg1):
return 1
def varargs(self, *args, **kargs):
return len(args)
alias_method = one_arg_method
DavidVilla-python-doublex-cddbcf289457/doublex/test3/ 0000775 0000000 0000000 00000000000 13451617403 0022472 5 ustar 00root root 0000000 0000000 DavidVilla-python-doublex-cddbcf289457/doublex/test3/__init__.py 0000664 0000000 0000000 00000000000 13451617403 0024571 0 ustar 00root root 0000000 0000000 DavidVilla-python-doublex-cddbcf289457/doublex/test3/test 0000777 0000000 0000000 00000000000 13451617403 0024504 2../test ustar 00root root 0000000 0000000 DavidVilla-python-doublex-cddbcf289457/doublex/test3/unit_tests.py 0000664 0000000 0000000 00000000670 13451617403 0025250 0 ustar 00root root 0000000 0000000 from unittest import TestCase
from doublex import Stub
# new on 1.8.3
# issue: https://bitbucket.org/DavidVilla/python-doublex/issues/25/support-from-python-35-type-hints-when
class TypeHintTests(TestCase):
def test_returning_type_hint(self):
class MyClass:
def get_name(self) -> str:
return 'a name'
with Stub(MyClass) as my_class:
my_class.get_name().returns('another name')
DavidVilla-python-doublex-cddbcf289457/doublex/tracer.py 0000664 0000000 0000000 00000003027 13451617403 0023264 0 ustar 00root root 0000000 0000000 # -*- coding:utf-8; tab-width:4; mode:python -*-
from .doubles import Stub
from .internal import Method, Property, WrongApiUsage
class MethodTracer(object):
def __init__(self, logger, method):
self.logger = logger
self.method = method
def __call__(self, *args, **kargs):
self.logger(str(self.method._create_invocation(args, kargs)))
class PropertyTracer(object):
def __init__(self, logger, prop):
self.logger = logger
self.prop = prop
def __call__(self, *args, **kargs):
propname = "%s.%s" % (self.prop.double._classname(), self.prop.key)
if args:
self.logger("%s set to %s" % (propname, args[0]))
else:
self.logger("%s gotten" % (propname))
class Tracer(object):
def __init__(self, logger):
self.logger = logger
def trace(self, target):
if isinstance(target, Method):
self.trace_method(target)
elif isinstance(target, Stub) or issubclass(target, Stub):
self.trace_class(target)
else:
raise WrongApiUsage('Can not trace %s' % target)
def trace_method(self, method):
method.attach(MethodTracer(self.logger, method))
def trace_class(self, double):
def attach_new_method(attr):
if isinstance(attr, Method):
attr.attach(MethodTracer(self.logger, attr))
elif isinstance(attr, Property):
attr.attach(PropertyTracer(self.logger, attr))
double._new_attr_hooks.append(attach_new_method)
DavidVilla-python-doublex-cddbcf289457/make 0000775 0000000 0000000 00000001431 13451617403 0020630 0 ustar 00root root 0000000 0000000 #!/usr/bin/make -f
# -*- mode:makefile -*-
URL_AUTH=svn+ssh://${ALIOTH_USER}@svn.debian.org/svn/python-modules/packages/doublex/trunk
URL_ANON=svn://svn.debian.org/svn/python-modules/packages/doublex/trunk
debian:
if [ ! -z "$${ALIOTH_USER}" ]; then \
svn co ${URL_AUTH} -N; \
else \
svn co ${URL_ANON} -N; \
fi
mv trunk/.svn .
rmdir trunk
svn up debian
.PHONY: docs doctests
docs:
$(MAKE) -C docs
doctests:
$(MAKE) -C doctests
wiki:
hg clone ssh://hg@bitbucket.org/DavidVilla/python-doublex/wiki
clean:
find . -name *.pyc -delete
find . -name *.pyo -delete
find . -name *~ -delete
$(RM) -r *.egg-info MANIFEST
$(RM) -r dist build *.egg-info .tox
$(RM) -r slides/reveal.js
$(MAKE) -C docs clean
$(MAKE) -C doctests clean
vclean: clean
$(RM) -r .svn debian
DavidVilla-python-doublex-cddbcf289457/pydoubles-site/ 0000775 0000000 0000000 00000000000 13451617403 0022736 5 ustar 00root root 0000000 0000000 DavidVilla-python-doublex-cddbcf289457/pydoubles-site/doublex-documentation 0000664 0000000 0000000 00000004151 13451617403 0027173 0 ustar 00root root 0000000 0000000 [migrated]
For the time being you can find the doublex API documentation at:
https://bitbucket.org/DavidVilla/python-doublex/wiki
What provides doublex respect to pyDoubles?
Respect to pyDoubles, doublex ...:
Use just hamcrest matchers (for all features).
Only ProxySpy requires an instance. Other doubles accept a class too, and they never instantiate it.
Stub observers: Notify arbitrary hooks when methods are invoked. Useful to add "side effects".
Stub delegates: Use callables, iterables or generators to create stub return values.
Mimic doubles: doubles that inherit the same collaborator subclasses. This provides full LSP for code that make strict type checking.
doublex support all the issues notified in the pyDoubles issue tracker:
And other features requested in the user group:
DavidVilla-python-doublex-cddbcf289457/pydoubles-site/downloads 0000664 0000000 0000000 00000001324 13451617403 0024653 0 ustar 00root root 0000000 0000000 Get latest release from here
gunzip doublex-X.X.tar.gz
tar xvf doublex-X.X.tar
cd doublex-X.X/
sudo python setup.py install
Or use pip:
$ sudo pip install doubles-X.X.tar.gz
Pydoubles is also available on Pypi :
$ sudo pip install doublex
You can also get the latest source code from the mercurial repository. Check out the project:
$ hg clone https://bitbucket.org/DavidVilla/python-doublex
Browse the source code, get support and notify bugs in the issue tracker .
DavidVilla-python-doublex-cddbcf289457/pydoubles-site/overview 0000664 0000000 0000000 00000010511 13451617403 0024525 0 ustar 00root root 0000000 0000000 [migrated]
What is pyDoubles?
pyDoubles is a test doubles framework for the Python platform. Test doubles frameworks are also called mocking frameworks. pyDoubles can be used as a testing tool or as a Test Driven Development tool.
It generates stubs, spies, and mock objects using a fluent interface that will make your unit tests more readable . Moreover, it's been designed to make your tests less fragile when possible.
The development of pyDoubles has been completely test-driven from scratch. The project is under continuous evolution, but you can extend the framework with your own requirements. The code is simple and well documented with unit tests.
What is doublex?
doublex is a new doubles framework that optionally provides the pyDoubles legacy API. It supports all the pyDoubles features and some more that can not be easely backported. If you are a pyDoubles user you can run your tests using doublex.pyDoubles module. However, we recommed the native doublex API for your new developments.
Supported test doubles
Find out what test doubles are according to Gerard Meszaros . pyDoubles offers mainly three kind of doubles:
Stub
Replaces the implementation of one or more methods in the object instance which plays the role of collaborator or dependency, returning the value that we explicitly write down in the test. A stub is actually a method but it is also common to use the noun stub for a class with stubbed methods. The stub does not have any kind or memory.
Stubs are used mainly for state validation or along with spies or mocks.
Spy
Replaces the implementation as a stub does, but it is also able to register and remember what methods are called during the test execution and how they are invoked.
They are used for interaction/behavior verification .
Mock
Contains the same features than the Stub and therefore the Spy, but it is very strict in the behavior specification it should expect from the System Under Tests. Before calling any method in the mock object, the framework should be told (in the test) which methods we expect to be called in order for them to succeed. Otherwise, the test will fail with an "UnexpectedBehavior" exception.
Mock objects are used when we have to be very precise in the behavior specification . They usually make the tests more fragile than a spy but still are necessary in many cases. It is common to use mock objects together with stubs in tests.
New to test doubles?
A unit test is comprised of three parts: Arrange/Act/Assert or Given/When/Then or whatever you want to call them. The scenario has to be created, exercised, and eventually we verify that the expected behavior happened. The test doubles framework is used to create the scenario (create the objects), and verify behavior after the execution but it does not make sense to invoke test doubles' methods in the test code. If you call the doubles' methods in the test code, you are testing the framework itself, which has been already tested (better than that, we crafted it using TDD). Make sure the calls to the doubles' methods happen in your production code.
Why another framework?
pyDoubles is inspired in mockito and jMock for Java, and also inspired in Rhino.Mocks for .Net. There are other frameworks for Python that work really well, but after some time using them, we were not really happy with the syntax and the readability of the tests. Fragile tests were also a problem. Some well-known frameworks available for Python are: mocker , mockito-python , mock , pymox .
pyDoubles is open source and free software, released under the Apache License Version 2.0
Take a look at the project's blog
DavidVilla-python-doublex-cddbcf289457/pydoubles-site/pydoubles-documentation 0000664 0000000 0000000 00000027177 13451617403 0027554 0 ustar 00root root 0000000 0000000 [migrated]
class SimpleExample(unittest.TestCase):
def test_ask_the_sender_to_send_the_report(self):
sender = spy(Sender())
service = SavingsService(sender)
service.analyze_month()
assert_that_method(sender.send_email).was_called(
).with_args('reports@x.com', ANY_ARG)
Import the framework in your tests
import unittest
from doublex.pyDoubles import *
If you are afraid of importing everything from the pyDoubles.framework module, you can use custom imports, although it has been carefully designed to not conflict with your own classes.
import unittest
from doublex.pyDoubles import stub, spy, mock
from doublex.pyDoubles import when, expect_call, assert_that_method
from doublex.pyDoubles import method_returning, method_raising
You can import Hamcrest matchers which are fully supported:
from hamcrest import *
Which doubles do you need?
You can choose to stub out a method in a regular object instance, to stub the whole object, or to create three types of spies and two types of mock objects.
Stubs
There are several ways to stub out methods.
Stub out a single method
If you just need to replace a single method in the collaborator object and you don't care about the input parameters, you can stub out just that single method:
collaborator = Collaborator() # create the actual object
collaborator.some_calculation = method_returning(10)
Now, when your production code invokes the method "some_calculation" in the collaborator object, the framework will return 10, no matter what parameters are passed in as the input.
If you want the method to raise an exception when called use this:
collaborator.some_calculation = method_raising(ApplicationException())
You can pass in any type of exception.
Stub out the whole object
Now the collaborator instance won't be the actual object but a replacement.
collaborator = stub(Collaborator())
Any method will return "None" when called with any input parameters.
If you want to change the return value you can use the "when" sentence:
when(collaborator.some_calculation).then_return(10)
Now, when your production code invokes "some_calculation" method, the stub will return 10, no matter what arguments are passed in.
You can also specify different return values depending on the input:
when(collaborator.some_calculation).with_args(5).then_return(10)
when(collaborator.some_calculation).with_args(10).then_return(20)
This means that "collaborator.some_calculation(5)" will return 10, and that it will return 20 when the input is 10. You can define as many input/output specifications as you want.
when(collaborator.some_calculation).with_args(5).then_return(10)
when(collaborator.some_calculation).then_return(20)
This time, "collaborator.some_calculation(5)" will return 10, and it will return 20 in any other case.
Any argument matches
The special keyword ANY_ARG is a wildcard for any argument in the
stubbed method:
when(collaborator.some_other_method).with_args(5, ANY_ARG).then_return(10)
The method "some_other_method" will return 10 as long as the first parameter is 5, no matter what the second parameter is. You can use any combination of "ANY_ARG" arguments. But remember that if all of them are ANY, you shouldn't specify the arguments, just use this:
when(collaborator.some_other_method).then_return(10)
It is also possible to make the method return exactly the first parameter passed in:
when(collaborator.some_other_method).then_return_input()
So this call: collaborator.some_other_method(10) wil return 10.
Matchers
You can also specify that arguments will match a certain function. Say that you want to return a value only if the input argument contains the substring "abc":
when(collaborator.some_method).with_args(
str_containing("abc")).then_return(10)
In the last release, pyDoubles matchers are just aliases for the hamcrest counterparts. See release notes .
Hamcrest Matchers
Since pyDoubles v1.2, we fully support Hamcrest matchers.
They are used exactly like pyDoubles matchers:
from hamcrest import *
from doublex.pyDoubles import *
def test_has_entry_matcher(self):
list = {'one':1, 'two':2}
when(self.spy.one_arg_method).with_args(
has_entry(equal_to('two'), 2)).then_return(1000)
assert_that(1000, equal_to(self.spy.one_arg_method(list)))
def test_all_of_matcher(self):
text = 'hello'
when(self.spy.one_arg_method).with_args(
all_of(starts_with('h'), instance_of(str))).then_return(1000)
assert_that(1000, equal_to(self.spy.one_arg_method(text)))
Note that the tests above are just showhing the pyDoubles framework working together with Hamcrest, they are not good examples of unit tests for your production code.
The method assert_that comes from Hamcrest, as well as the matchers: has_entry, equal_to, all_of, starts_with, instance_of.
Notice that all_of and any_of, allow you to define more than one matcher for a single argument, which is really powerful.
For more informacion on matchers, read this blog post .
Stub out the whole unexisting object
If the Collaborator class does not exist yet, or you don't want the framework to check that the call to the stub object method matches the actual API in the actual object, you can use an "empty" stub.
collaborator = empty_stub()
when(collaborator.alpha_operation).then_return("whatever")
The framework is creating the method "alpha_operation" dynamically
and making it return "whatever".
The use of empty_stub, empty_spy or empty_mock is not recommended because you lose the API match check. We only use them as the construction of the object is too complex among other circumstances.
Spies
Please read the documentation above about stubs, because the API to
define method behaviors is the same for stubs and spies. To create
the object:
collaborator = spy(Collaborator())
After the execution of the system under test, we want to validate
that certain call was made:
assert_that_method(collaborator.send_email).was_called()
That will make the test pass if method "send_email" was invoked one or more times, no matter what arguments were passed in.
We can also be precise about the arguments:
assert_that_method(collaborator.send_email).was_called().with_args("example@iexpertos.com")
Notice that you can combine the "when" statement with the called assertion:
def test_sut_asks_the_collaborator_to_send_the_email(self):
sender = spy(Sender())
when(sender.send_email).then_return(SUCCESS)
object_under_test = Sut(sender)
object_under_test.some_action()
assert_that_method(
sender.send_email).was_called().with_args("example@iexpertos.com")
Any other call to any method in the "sender" double will return "None" and will not interrupt the test. We are not telling all that happens between the sender and the SUT, we are just asserting on what we want to verify.
The ANY_ARG matcher can be used to verify the call as well:
assert_that_method(collaborator.some_other_method).was_called().with_args(5, ANY_ARG)
Matchers can also be used in the assertion:
assert_that_method(collaborator.some_other_method).was_called().with_args(5, str_containing("abc"))
It is also possible to assert that wasn't called using:
assert_that_method(collaborator.some_method).was_never_called()
You can assert on the number of times a call was made:
assert_that_method(collaborator.some_method).was_called().times(2)
assert_that_method(collaborator.some_method).was_called(
).with_args(SOME_VALUE, OTHER_VALUE).times(2)
You can also create an "empty_spy" to not base the object in a
certain instance:
sender = empty_spy()
The ProxySpy
There is a special type of spy supported by the framework which
is the ProxySpy:
collaborator = proxy_spy(Collaborator())
The proxy spy will record any call made to the object but rather than replacing the actual methods in the actual object, it will execute them. So the actual methods in the Collaborator will be invoked by default. You can replace the methods one by one using the "when" statement:
when(collaborator.some_calculation).then_return(1000)
Now "some_calculation" method will be a stub method but the remaining methods in the class will be the regular implementation.
The ProxySpy might be interesting when you don't know what the actual method will return in a given scenario, but still you want to check that some call is made. It can be used for debugging purposes.
Mocks
Before calls are made, they have to be expected:
def test_sut_asks_the_collaborator_to_send_the_email(self):
sender = mock(Sender())
expect_call(sender.send_email)
object_under_test = Sut(sender)
object_under_test.some_action()
sender.assert_that_is_satisfied()
The test is quite similar to the one using a spy. However the framework behaves different. If any other call to the sender is made during "some_action", the test will fail. This makes the test more fragile. However, it makes sure that this interaction is the only one between the two objects, and this might be important for you.
More precise expectations
You can also expect the call to have certain input parameters:
expect_call(sender.send_email).with_args("example@iexpertos.com")
Setting the return of the expected call
Additionally, if you want to return anything when the expected call
occurs, there are two ways:
expect_call(sender.send_email).returning(SUCCESS)
Which will return SUCCESS whatever arguments you pass in, or
expect_call(sender.send_email).with_args("wrong_email").returning(FAILURE)
Which expects the method to be invoked with "wrong_email" and will return FAILURE.
Mocks are strict so if you expect the call to happen several times, be explicit with that:
expect_call(sender.send_email).times(2)
expect_call(sender.send_email).with_args("admin@iexpertos.com").times(2)
Make sure the "times" part is at the end of the sentence:
expect_call(sender.send_email).with_args("admin@iexpertos.com").returning('OK').times(2)
As you might have seen, the "when" statement is not used for mocks, only for stubs and spies. Mock objects use the "expect_call" syntax together with the "assert_that_is_satisfied"
(instance method).
More documentation
The best and most updated documentation are the unit tests of the framework itself. We encourage the user to read the tests and see what features are supported in every commit into the source code repository:
pyDoublesTests/unit.py
You can also read about what's new in every release in the blog
DavidVilla-python-doublex-cddbcf289457/pydoubles-site/release-notes 0000664 0000000 0000000 00000006072 13451617403 0025434 0 ustar 00root root 0000000 0000000
doublex 1.6.5
doublex 1.6.4
Asynchronous spy assertion race condition bug fixed.
Reading double attributes returns collaborator.class attribute values by default.
doublex 1.6.2
Invocation stubbed return value is now stored.
New low level spy API: double method "calls" property provides access to invocations and their argument values. Each 'call' has an "args" sequence and "kargs dictionary". This provides support to perform individual assertions and direct access to invocation argument values. (see test and doc ).
doublex 1.6
First release supporting Python-3 (up to Python-3.2) [fixes issue 7 ].
Ad-hoc stub attributes (see test ).
Partial support for non native Python functions.
ProxySpy propagated stubbed invocations too (see test ).
doublex 1.5.1
This release includes support for asynchronous spy assertions. See this blog post for the time being, soon in the official documentation.
doublex/pyDoubles 1.5
Since this release the pyDoubles API is provided as a wrapper to doublex . However, there are small differences. pyDoubles matchers are not supported anymore, although you may get the same feature using standard hamcrest matchers. Anyway, legacy pyDoubles matchers are provided as hamcrest aliases.
In most cases the only required change in your code is the module name, that change from:
from pyDoubles.framework.*
to:
from doublex.pyDoubles import *
If you have problems migrating to the new 1.5 release or migrating from pyDoubles to doublex, please ask for help in the discussion forum or in the issue tracker .
DavidVilla-python-doublex-cddbcf289457/pydoubles-site/support 0000664 0000000 0000000 00000001157 13451617403 0024401 0 ustar 00root root 0000000 0000000 Free support
Mailing list: http://groups.google.com/group/pydoubles
Issue tracker, mercurial repository:
https://bitbucket.org/carlosble/pydoubles/overview
Thanks to BitBucket!
Commercial support
The development team of pyDoubles is a software company based in Spain. We are happy to help other companies with the usage and extension of pyDoubles. If you want to have custom features or direct support, please contact us at info@iexpertos.com
DavidVilla-python-doublex-cddbcf289457/requirements.txt 0000664 0000000 0000000 00000000017 13451617403 0023250 0 ustar 00root root 0000000 0000000 PyHamcrest
six
DavidVilla-python-doublex-cddbcf289457/setup.py 0000775 0000000 0000000 00000005154 13451617403 0021510 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# Copyright (C) 2012-2018 David Villa Alises
#
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import os
import sys
from setuptools import setup, find_packages
# hack to prevent 'test' target exception:
# http://www.eby-sarna.com/pipermail/peak/2010-May/003357.html
import multiprocessing, logging
exec(open('version.py').read())
def local_open(fname):
return open(os.path.join(os.path.dirname(__file__), fname))
exec(local_open('version.py').read())
config = dict(
name = 'doublex',
version = __version__,
description = 'Python test doubles',
keywords = ['unit tests', 'doubles', 'stub', 'spy', 'mock'],
author = 'David Villa Alises',
author_email = 'David.Villa@gmail.com',
url = 'https://bitbucket.org/DavidVilla/python-doublex',
packages = ['doublex'],
data_files = [('', ['README.rst'])],
test_suite = 'doublex.test',
license = 'GPLv3',
long_description = local_open('README.rst').read(),
install_requires = local_open('requirements.txt').readlines(),
classifiers = [
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Topic :: Software Development',
'Topic :: Software Development :: Quality Assurance',
'Topic :: Software Development :: Testing',
],
)
if sys.version_info >= (3,):
config.update(dict(
test_suite = 'doublex.test3',
))
setup(**config)
DavidVilla-python-doublex-cddbcf289457/slides/ 0000775 0000000 0000000 00000000000 13451617403 0021251 5 ustar 00root root 0000000 0000000 DavidVilla-python-doublex-cddbcf289457/slides/Makefile 0000664 0000000 0000000 00000000312 13451617403 0022705 0 ustar 00root root 0000000 0000000
LINKS=css lib plugin js
all: reveal.js $(LINKS)
reveal.js:
git clone --depth 1 https://github.com/hakimel/reveal.js.git
$(LINKS):
ln -s reveal.js/$@ $@
clean:
$(RM) -r reveal.js
$(RM) $(LINKS)
DavidVilla-python-doublex-cddbcf289457/slides/beamer/ 0000775 0000000 0000000 00000000000 13451617403 0022504 5 ustar 00root root 0000000 0000000 DavidVilla-python-doublex-cddbcf289457/slides/beamer/Makefile 0000664 0000000 0000000 00000000026 13451617403 0024142 0 ustar 00root root 0000000 0000000 include arco/latex.mk
DavidVilla-python-doublex-cddbcf289457/slides/beamer/custom.sty 0000664 0000000 0000000 00000003236 13451617403 0024563 0 ustar 00root root 0000000 0000000 \usepackage[spanish]{babel}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{times}
\usepackage{graphicx}
\graphicspath{{figures/}}
\usepackage{listings}
\newcommand{\lstfont}{\ttfamily\fontfamily{pcr}}
\lstset{
% frame
frame = Ltb,
framerule = 0pt,
aboveskip = 0.5cm,
framextopmargin = 3pt,
framexbottommargin = 3pt,
framexleftmargin = 0.4cm,
framesep = 0pt,
rulesep = .4pt,
%--
% caption
belowcaptionskip = 5pt,
%--
% text style
stringstyle = \ttfamily,
showstringspaces = false,
basicstyle = \scriptsize\lstfont,
keywordstyle = \bfseries,
%--
% numbers
numbers = none,
numbersep = 15pt,
numberstyle = \scriptsize\lstfont,
numberfirstline = false,
%
breaklines = true,
emptylines = 1, % several empty lines are shown as one
%
literate = % caracteres especiales
{á}{{\'a}}1 {Á}{{\'A}}1
{é}{{\'e}}1 {É}{{\'E}}1
{í}{{\'i}}1 {Í}{{\'I}}1
{ó}{{\'o}}1 {Ó}{{\'O}}1
{ú}{{\'u}}1 {Ú}{{\'U}}1
{ñ}{{\~n}}1 {Ñ}{{\~N}}1
{¡}{{!`}}1 {¿}{{?`}}1
}
\parskip=10pt
\mode
{
\usetheme{Frankfurt}
\setbeamercovered{transparent}
% or whatever (possibly just delete it)
}
% Delete this, if you do not want the table of contents to pop up at
% the beginning of each subsection:
\AtBeginSubsection[]
{
\begin{frame}{Outline}
\tableofcontents[currentsection,currentsubsection]
\end{frame}
}
% If you wish to uncover everything in a step-wise fashion, uncomment
% the following command:
%\beamerdefaultoverlayspecification{<+->}
DavidVilla-python-doublex-cddbcf289457/slides/beamer/slides.tex 0000664 0000000 0000000 00000022714 13451617403 0024517 0 ustar 00root root 0000000 0000000 \documentclass[11pt]{beamer}
\usepackage{custom}
\title{doublex: Python test doubles framework}
\subtitle{\bfseries\small\url{http://bitbucket.org/DavidVilla/python-doublex}}
\author{\bfseries\small\texttt{@david\_vi11a}}
%\institute{}
\date{}
%\subject{}
\begin{document}
\begin{frame}
\titlepage
\end{frame}
\begin{frame}{Contents}
\tableofcontents
\end{frame}
\section{Intro}
\begin{frame}{Another doubles library for Python?}
Yes, why not?
\end{frame}
\begin{frame}{doublex features}
\begin{itemize}
\item Stubs
\item Spies
\item Mocks
\item ah hoc stub methods
\item stub delegates
\item stub observers
\item properties
\item hamcrest matchers for \textbf{all} assertions
\item wrapper for legacy pyDoubles API
\item doublex never instantiates your classes!
\end{itemize}
\end{frame}
\section{Stubs}
\begin{frame}[fragile]{Stubs}
\framesubtitle{set fixed return value}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
class Collaborator:
def add(self, x, y):
return x + y
with Stub(Collaborator) as stub:
stub.add(ANY_ARG).returns(1000)
assert_that(stub.add(2, 2), is_(1000))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Stubs}
\framesubtitle{\... by calling arg values}
Undefined stub methods returns None.
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
with Stub(Collaborator) as stub:
stub.add(2, 2).returns(1000)
stub.add(3, ANY_ARG).returns(0)
assert_that(stub.add(1, 1), is_(None))
assert_that(stub.add(2, 2), is_(1000))
assert_that(stub.add(3, 0), is_(0))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Stubs}
\framesubtitle{\... by hamcrest matcher}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
with Stub(Collaborator) as stub:
stub.add(2, greater_than(4)).returns(4)
assert_that(stub.add(2, 1), is_(None))
assert_that(stub.add(2, 5), is_(4))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Stubs}
\framesubtitle{\... by composite hamcrest matcher}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
with Stub(Collaborator) as stub:
stub.foo(has_length(all_of(
greater_than(4), less_than(8)))).returns(1000)
assert_that(stub.add(2, "bad"), is_(None))
assert_that(stub.add(2, "enough"), is_(1000))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\section{Spies}
\begin{frame}[fragile]{Spies}
\framesubtitle{checking called methods}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
spy = Spy(Collaborator)
spy.add(2, 3)
spy.add("hi", 3.0)
spy.add([1, 2], 'a')
assert_that(spy.add, called())
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Spies}
\framesubtitle{collaborator signature checking}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
spy = Spy(Collaborator)
spy.add()
TypeError: __main__.Collaborator.add() takes
exactly 3 arguments (1 given)
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Spies}
\framesubtitle{checking called times (with matcher too!)}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
spy = Spy(Collaborator)
spy.add(2, 3)
spy.add("hi", 3.0)
spy.add([1, 2], 'a')
assert_that(spy.add, called().times(3))
assert_that(spy.add, called().times(greater_than(2)))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Spies}
\framesubtitle{filter by argument value: \texttt{with\_args()})}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
spy = Spy(Collaborator)
spy.add(2, 3)
spy.add(2, 8)
spy.add("hi", 3.0)
assert_that(spy.add, called().with_args(2, ANY_ARG)).times(2)
assert_that(spy.add, never(called().with_args(0, 0)))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Spies}
\framesubtitle{filter by key argument (with matcher)}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
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")))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Spies}
\framesubtitle{Verbose meaning-full report messages!}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
spy = Spy()
spy.foo(1)
spy.bar("hi")
assert_that(spy.foo, called().with_args(4))
AssertionError:
Expected: these calls:
Spy.foo(4)
but: calls that actually ocurred were:
Spy.foo(1)
Spy.bar('hi')
\end{lstlisting}
\end{exampleblock}
\end{frame}
\subsection{ProxySpy}
\begin{frame}[fragile]{ProxySpy}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
with ProxySpy(Collaborator()) as spy:
spy.add(2, 2).returns(1000)
assert_that(spy.add(2, 2), is_(1000))
assert_that(spy.add(1, 1), is_(2))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\section{Mocks}
\begin{frame}[fragile]{Mocks}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
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?")
assert_that(smtp.data("I am afraid.."), is_(True))
assert_that(smtp, verify())
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Mocks}
\framesubtitle{invocation order is important}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
with Mock() as mock:
mock.foo()
mock.bar()
mock.bar()
mock.foo()
assert_that(mock, verify())
AssertionError:
Expected: these calls:
Mock.foo()
Mock.bar()
but: calls that actually ocurred were:
Mock.bar()
Mock.foo()
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Mocks}
\framesubtitle{unless you do not mind: \texttt{any\_order\_verify()}}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
with Mock() as mock:
mock.foo()
mock.bar()
mock.bar()
mock.foo()
assert_that(mock, any_order_verify())
\end{lstlisting}
\end{exampleblock}
\end{frame}
\section{ah hoc stub methods}
\begin{frame}[fragile]{ah hoc stub methods}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
collaborator = Collaborator()
collaborator.foo = method_returning('bye')
assert_that(self.collaborator.foo(), is_('bye'))
collaborator.foo = method_raising(SomeException)
collaborator.foo()
SomeException:
\end{lstlisting}
\end{exampleblock}
\end{frame}
\section{stub observers}
\begin{frame}[fragile]{stub observers}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
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))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\section{stub delegates}
\begin{frame}[fragile]{stub delegates}
\framesubtitle{delegating to callables}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
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"))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{stub delegates}
\framesubtitle{delegating to iterables}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
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))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\section{properties}
\begin{frame}[fragile]{stubbing properties}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
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
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{spying properties (with matchers!)}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
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))))
\end{lstlisting}
\end{exampleblock}
\end{frame}
\section{Mimics}
\begin{frame}[fragile]{normal doubles support only duck-typing}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
class A(object):
pass
class B(A):
pass
>>> spy = Spy(B())
>>> isinstance(spy, Spy)
True
>>> isinstance(spy, B)
False
\end{lstlisting}
\end{exampleblock}
\end{frame}
\begin{frame}[fragile]{Mimics support full LSP}
\begin{exampleblock}{}
\begin{lstlisting}[language=Python]
>>> spy = Mimic(Spy, B)
>>> isinstance(spy, B)
True
>>> isinstance(spy, A)
True
>>> isinstance(spy, Spy)
True
>>> isinstance(spy, Stub)
True
>>> isinstance(spy, object)
True
\end{lstlisting}
\end{exampleblock}
\end{frame}
\section{Questions}
\begin{frame}{}
\begin{center}
{\huge Questions?}
\end{center}
\end{frame}
\end{document}
%% Local Variables:
%% coding: utf-8
%% mode: flyspell
%% ispell-local-dictionary: "american"
%% End:
DavidVilla-python-doublex-cddbcf289457/slides/index.html 0000664 0000000 0000000 00000043712 13451617403 0023255 0 ustar 00root root 0000000 0000000
doublex
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))))
DavidVilla-python-doublex-cddbcf289457/slides/sample.py 0000775 0000000 0000000 00000013406 13451617403 0023113 0 ustar 00root root 0000000 0000000 #!/usr/bin/python
# -*- mode:python; coding:utf-8; tab-width:4 -*-
from unittest import TestCase
from doublex import (Stub, Spy, ProxySpy, Mock,
assert_that, called, ANY_ARG, never,
verify, any_order_verify)
from hamcrest import greater_than, anything, contains_string
class AlreadyExists(Exception):
pass
class InvalidPassword(Exception):
pass
class AccountStore:
def save(self, login, password):
pass
def has_user(self, login):
pass
class Group(set):
def __init__(self, name):
pass
class PasswordService:
def generate(self):
pass
class AccountService:
def __init__(self, store, password_service):
self.store = store
self.password_service = password_service
def create_user(self, login):
if self.store.has_user(login):
raise AlreadyExists()
password = self.password_service.generate()
if not password:
raise InvalidPassword()
self.store.save(login, password)
def create_group(self, group_name, user_names):
group = Group(group_name)
for name in user_names:
try:
self.create_user(name)
except AlreadyExists:
pass
group.add(name)
class AccountTests(TestCase):
def test_account_creation__free_stub(self):
with Stub() as password_service:
password_service.generate().returns('some')
store = Spy(AccountStore)
service = AccountService(store, password_service)
service.create_group('team', ['John', 'Peter', 'Alice'])
assert_that(store.save, called())
def test_account_creation__restricted_stub(self):
with Stub(PasswordService) as password_service:
password_service.generate().returns('some')
store = Spy(AccountStore)
service = AccountService(store, password_service)
service.create_user('John')
assert_that(store.save, called())
def test_account_creation__3_accounts(self):
with Stub(PasswordService) as password_service:
password_service.generate().returns('some')
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)))
def test_account_creation__argument_values(self):
with Stub(PasswordService) as password_service:
password_service.generate().returns('some')
store = Spy(AccountStore)
service = AccountService(store, password_service)
service.create_user('John')
assert_that(store.save, called().with_args('John', 'some'))
assert_that(store.save, called().with_args('John', ANY_ARG))
assert_that(store.save, never(called().with_args('Alice', anything())))
assert_that(store.save,
called().with_args(contains_string('oh'), ANY_ARG))
def test_account_creation__report_message(self):
with Stub(PasswordService) as password_service:
password_service.generate().returns('some')
store = Spy(AccountStore)
service = AccountService(store, password_service)
service.create_group('team', ['John', 'Alice'])
# assert_that(store.save, called().with_args('Peter'))
def test_account_already_exists(self):
with Stub(PasswordService) as password_service:
password_service.generate().returns('some')
with ProxySpy(AccountStore()) as store:
store.has_user('John').returns(True)
service = AccountService(store, password_service)
with self.assertRaises(AlreadyExists):
service.create_user('John')
def test_account_behaviour_with_mock(self):
with Stub(PasswordService) as password_service:
password_service.generate().returns('some')
with Mock(AccountStore) as store:
store.has_user('John')
store.save('John', 'some')
store.has_user('Peter')
store.save('Peter', 'some')
service = AccountService(store, password_service)
service.create_group('team', ['John', 'Peter'])
assert_that(store, verify())
# def test_account_behaviour_with_mock_any_order(self):
# with Stub(PasswordService) as password_service:
# password_service.generate().returns('some')
#
# with Mock(AccountStore) as store:
# store.has_user('John')
# store.has_user('Peter')
# store.save('John', 'some')
# store.save('Peter', 'some')
#
# service = AccountService(store, password_service)
#
# service.create_user('John')
# service.create_user('Peter')
#
# assert_that(store, any_order_verify())
def test_stub_delegates(self):
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'))
def test_stub_delegates_list(self):
with Stub(PasswordService) as password_service:
password_service.generate().delegates(["12345", "mypass", "nothing"])
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', 'nothing'))
DavidVilla-python-doublex-cddbcf289457/tox.ini 0000664 0000000 0000000 00000000514 13451617403 0021301 0 ustar 00root root 0000000 0000000 [tox]
envlist = py26, py27, py33, py34, py35, py36, py37
[testenv]
deps=nose
commands=python setup.py test
[testenv:py27]
deps=nose
future
# using 2to3 with nose
#:https://bitbucket.org/kumar303/fudge/src/9cafce359a21/tox.ini#cl-26
#[testenv:py34]
# https://bitbucket.org/hpk42/tox/issue/127/possible-problem-with-python34
DavidVilla-python-doublex-cddbcf289457/version.py 0000664 0000000 0000000 00000000026 13451617403 0022023 0 ustar 00root root 0000000 0000000 __version__ = '1.9.2'