------------- Documentation ------------- .. sourcecode:: python 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 ================================== .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python when(collaborator.some_other_method).then_return(10) It is also possible to make the method return exactly the first parameter passed in: .. sourcecode:: python 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": .. sourcecode:: python when(collaborator.some_method).with_args( str_containing("abc")).then_return(10) Hamcrest Matchers ^^^^^^^^^^^^^^^^^ Since pyDoubles v1.2, we fully support `Hamcrest `_ matchers. They are used exactly like pyDoubles matchers: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python collaborator = spy(Collaborator()) After the execution of the system under test, we want to validate that certain call was made: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python assert_that_method(collaborator.some_other_method).was_called().with_args(5, ANY_ARG) Matchers can also be used in the assertion: .. sourcecode:: python 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: .. sourcecode:: python assert_that_method(collaborator.some_method).was_never_called() You can assert on the number of times a call was made: .. sourcecode:: python 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: .. sourcecode:: python sender = empty_spy() The ProxySpy ^^^^^^^^^^^^ There is a special type of spy supported by the framework which is the ProxySpy: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python expect_call(sender.send_email).returning(SUCCESS) Which will return SUCCESS whatever arguments you pass in, or: .. sourcecode:: python 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: .. sourcecode:: python 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: .. sourcecode:: python 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 `_. .. Local Variables: .. coding: utf-8 .. mode: rst .. mode: flyspell .. ispell-local-dictionary: "american" .. fill-columnd: 90 .. End: