derwiki-adroll / mock

Automatically exported from code.google.com/p/mock
BSD 2-Clause "Simplified" License
0 stars 0 forks source link

Assertion for Chained method calls #73

Closed GoogleCodeExporter closed 9 years ago

GoogleCodeExporter commented 9 years ago
This is an enhancement request to add some syntactic sugar for asserting 
against mocks when chained method calls occur.

What steps will reproduce the problem?

Using the existing mock library, to make assertions when chained method calls 
are present, something like the following must be done:

    def test_oldschool_chained_assertions(self):
        def _test_func(my_mock):
            return my_mock.first().second().third()

        some_mock = Mock()
        result = _test_func(some_mock)

        first_mock = some_mock.first.return_value
        second_mock = first_mock.second.return_value
        final_mock = second_mock.third.return_value
        self.assertEqual(result, final_mock)

What is the expected output? What do you see instead?

A possible way to make this scenario easier to code and easier to read would be 
to add an assertion to the mock library to make it look similar to asserting 
against method_calls like so:

    def test_chain_is_matched(self):
        my_mock = Mock()
        result = my_mock.first().second().third()

        my_mock.assert_call_chain([
            ('first', (), {}),
            ('second', (), {}),
            ('third', (), {}),
        ], result)

This new assertion takes 2 arguments, 1 the list of (chained) method calls, and 
2 the expected value to be returned at the end of the call chain.

What version of the product are you using? On what operating system?
trunk, all operating systems

Please provide any additional information below.

The implementation is fairly simple, have the new assertion recursively follow 
the return values as long as the method_calls match what is passed into the new 
assertion and once the end of the call chain is reached make sure the final 
return value matches the return value argument. See attachment for 
implementation and tests.

I've been using this for a few months now in my own separate library in the 
context of mocking Django's ORM and it has been a very nice feature to have 
available.

Original issue reported on code.google.com by mattj.mo...@gmail.com on 10 Mar 2011 at 1:29

Attachments:

GoogleCodeExporter commented 9 years ago
@fuzzyman - what are your thoughts about this?  Do you think this is something 
that would be useful to the masses or do you think this should belong in a 
separate extension library somewhere outside of mock, or lastly do you think 
this should just plain not exist?

Original comment by mattj.mo...@gmail.com on 1 Jul 2011 at 2:38

GoogleCodeExporter commented 9 years ago
I don't understand why you would assert the result, when the result of a 
chained mock call will be whatever you set it to be?

Original comment by fuzzyman on 17 Jul 2011 at 1:08

GoogleCodeExporter commented 9 years ago

Original comment by fuzzyman on 17 Jul 2011 at 1:08

GoogleCodeExporter commented 9 years ago
Take a look at this example:

def somefunc():
    return SomeThing('x').first('1').second(2).third().final()

If i want a test to assert that I called SomeThing with 'x', first with '1', 
second with 2 third with nothing and final with nothing, then return whatever 
final returns I would have to do the following things in my test:

SomeThing.return_value.first.return_value.second.return_value.third.return_value
.final.return_value = 'What I Expect'

result = somefunc()

assert result == 'What I Expect'
assert SomeThing.call_args ...
assert SomeThing.return_value.first.call_args ...
assert SomeThing.return_value.first.return_value.second.call_args ...
assert 
SomeThing.return_value.first.return_value.second.return_value.third.call_args 
...
assert 
SomeThing.return_value.first.return_value.second.return_value.third.return_value
.final.call_args ...

Without asserting against the result, removing the return in somefunc would not 
fail the test, also chaining another function call onto the end of 
final.return_value would not cause the test to fail. Also, in this scenario I 
don't really care what the return_value of final is, as long as that is what 
comes back from somefunc my test will pass, so I don't really even need to 
explicitly set the return value if I can somehow get a hold of whatever Mock 
object that return value was.

The other benefit of asserting against the return value is that currently, in 
this example, if I change the chain order or add or remove anything from the 
chained call I will now need to update my test in at least 2 places. Once in 
the setup, once for the last assertion against final's call_args and 
potentially some of the other assertions depending on how somefunc changed. 
With the chained assertion checking the return value you can eliminate the 
duplication between the setup and assertions (and potentially eliminate the 
setup all together) of the test and it would become:

result = somefunc()

my_mock.assert_call_chain([
   ('first', ('1',), {}),
   ('second', (2,), {}),
   ('third', (), {}),
   ('final', (), {}),
], result)

Does that paint a better picture of possible uses of this?

Original comment by mattj.mo...@gmail.com on 17 Jul 2011 at 3:01

GoogleCodeExporter commented 9 years ago
Ok, interesting.

I agree that asserting multiple / chained calls *should* be simpler in mock. 
I'm not yet convinced this is how it should look. 

My current idea is to integrate the new 'call' object with the new 'mock_calls' 
attribute (from issue 82 - not yet implemented) that makes all sorts of 
assertions simpler.

Original comment by fuzzyman on 17 Jul 2011 at 5:05

GoogleCodeExporter commented 9 years ago
The new `mock_calls` functionality is implemented on head. It supports 
assertions for chained calls using the (also new) `call` object. 

{{{
>>> from mock import Mock, call
>>> mock = Mock()
>>> mock.foo(1).bar()().baz.beep(a=6)
<mock.Mock object at 0x519bf0>
>>> this_call = call.foo(1).bar()().baz.beep(a=6)
>>> assert mock.mock_calls == this_call.call_list()
>>> this_call
<call name='foo().bar()().baz.beep()' values=('foo().bar()().baz.beep', (), 
{'a': 6})>
>>> 
>>> mock = Mock()
>>> mock.a(1, 2)
<mock.Mock object at 0x519150>
>>> mock.b(3, 4)
<mock.Mock object at 0x519030>
>>> mock.c(5, 6)
<mock.Mock object at 0x519610>
>>> assert mock.mock_calls == [call.a(1, 2), call.b(3, 4), call.c(5, 6)]
}}}

It doesn't support return value assertions, so your helper function still has 
that advantage. `mock_calls` tracks all calls, so it makes it simpler to make 
assertions about multiple calls as well as chained calls. It also includes 
calls to magic methods.

It is intended to replace `call_args_list` and `method_calls`, providing "one 
place" to go and check all calls.

Original comment by fuzzyman on 18 Jul 2011 at 12:06

GoogleCodeExporter commented 9 years ago
Nice, I like it. 

Original comment by mattj.mo...@gmail.com on 18 Jul 2011 at 12:22

GoogleCodeExporter commented 9 years ago
This feature request has been filled by the mock_calls and the call object for 
asserting chained calls.

Original comment by fuzzyman on 14 Nov 2013 at 11:13