[openstack-dev] [all] In defence of faking

Jay Pipes jaypipes at gmail.com
Mon Sep 22 20:09:25 UTC 2014


On 09/22/2014 03:36 PM, Robert Collins wrote:
> On 23 September 2014 03:58, Matthew Booth <mbooth at redhat.com> wrote:
>> If you missed the inaugural OpenStack Bootstrapping Hour, it's here:
>> http://youtu.be/jCWtLoSEfmw . I think this is a fantastic idea and big
>> thanks to Sean, Jay and Dan for doing this. I liked the format, the
>> informal style and the content. Unfortunately I missed the live event,
>> but I can confirm that watching it after the event worked just fine
>> (thanks for reading out live questions for the stream!).
>>
>> I'd like to make a brief defence of faking, which perhaps predictably in
>> a talk about mock took a bit of a bashing.
>>
>> Firstly, when not to fake. As Jay pointed out, faking adds an element of
>> complexity to a test, so if you can achieve what you need to with a
>> simple mock then you should. But, as the quote goes, you should "make
>> things as simple as possible, but not simpler."
>>
>> Here are some simple situations where I believe fake is the better solution:
>>
>> * Mock assertions aren't sufficiently expressive on their own
>>
>> For example, imagine your code is calling:
>>
>> def complex_set(key, value)
>>
>> You want to assert that on completion of your unit, the final value
>> assigned to <key> was <value>. This is difficult to capture with mock
>> without risking false assertion failures if complex_set sets other keys
>> which you aren't interested in, or if <key>'s value is set multiple
>> times, but you're only interested in the last one. A little fake
>> function which stores the final value assigned to <key> does this simply
>> and accurately without adding a great deal of complexity. e.g.
>>
>> mykey = [None]
>> def fake_complex_set(key, value):
>>    if key == 'FOO':
>>      mykey[0] = value
>>
>> with mock.patch.object(unit, 'complex_set', side_effect=fake_complex_set):
>>    run_test()
>> self.assertEquals('expected', mykey[0])
>>
>> Summary: fake method is a custom mock assertion.
>>
>> * A simple fake function is simpler than a complex mock dance
>>
>> For example, you're mocking 2 functions: start_frobincating(key) and
>> wait_for_frobnication(key). They can potentially be called overlapping
>> with different keys. The desired mock return value of one is dependent
>> on arguments passed to the other. This is better mocked with a couple of
>> little fake functions and some external state, or you risk introducing
>> artificial constraints on the code under test.
>>
>> Jay pointed out that faking methods creates more opportunities for
>> errors. For this reason, in the above cases, you want to keep your fake
>> function as simple as possible (but no simpler). However, there's a big
>> one: the fake driver!
>>
>> This may make less sense outside of driver code, although faking the
>> image service came up in the talk. Without looking at the detail, that
>> doesn't necessarily sound awful to me, depending on context. In the
>> driver, though, the ultimate measure of correctness isn't a Nova call:
>> it's the effect produced on the state of an external system.
>>
>> For the VMware driver we have nova.tests.virt.vmwareapi.fake. This is a
>> lot of code: 1599 lines as of writing. It contains bugs, and it contains
>> inaccuracies, and both of these can mess up tests. However:
>>
>> * It's vastly simpler than the system it models (vSphere server)
>> * It's common code, so gets fixed over time
>> * It allows tests to run almost all driver code unmodified
>>
>> So, for example, it knows that you can't move a file before you create
>> it. It knows that creating a VM creates a bunch of different files, and
>> where they're created. It knows what objects are created by the server,
>> and what attributes they have. And what attributes they don't have. If
>> you do an object lookup, it knows which objects to return, and what
>> their properties are.
>>
>> All of this knowledge is vital to testing, and if it wasn't contained in
>> the fake driver, or something like it[1], would have to be replicated
>> across all tests which require it. i.e. It may be 1599 lines of
>> complexity, but it's all complexity which has to live somewhere anyway.
>>
>> Incidentally, this is fresh in my mind because of
>> https://review.openstack.org/#/c/122760/ . Note the diff stat: +70,
>> -161, and the rewrite has better coverage, too :) It executes the
>> function under test, it checks that it produces the correct outcome, and
>> other than that it doesn't care how the function is implemented.
>>
>> TL;DR
>>
>> * Bootstrap hour is awesome
>> * Don't fake if you don't have to
>> * However, there are situations where it's a good choice
>
> I'm going to push on this a bit further. Mocking is fine in many
> cases, but it encodes dependencies from within your objects into the
> test suite: e.g. that X will call Y and then Z - and if thats what you
> want to *test*, thats great, but it often isn't outside of the very
> smallest unit tests - because it makes your tests fragile to change -
> tests for a class Foo need to change when a parent class Bar alters,
> or when a utility class Quux alters. And that coupling has a cost and
> is a pain.

It's a pain that is on purpose, though. The coupling of the unit test to 
the class relationship is precisely to cause pain when the interface 
changes. It is this pain that informs you that "oh, I changed an 
interface, so I need to change the unit test that verifies that interface".

The problem we have in OpenStack right now is too many fake stub methods 
that hide too much, either using stuff like:

  def fake_get_instance(*args, **kwargs):
     ... blah

or returning completely incorrect stuff (like dicts instead of objects, 
thus in some cases making our unit tests the *only* remaining place 
where certain things are passed as dicts and not objects).

Also, for the record, when I refer to fakes, I'm mostly referring to 
stubbed out methods that really should instead be calls to 
mock.MagicMock.assert_called[_once]_with().

I'm not referring to fake drivers. Those are a special kind of problem 
in OpenStack, but not related to the use of fake stub methods when mock 
should be used instead.

The problem with some fake drivers in OpenStack code is demonstrated here:

https://github.com/openstack/nova/blob/e7d4ede1317ca0401bf8b4d49d58d000c53b1ed2/nova/tests/image/fake.py#L36

* The fixtures contain types that don't actually come back from Glance 
(for example size isn't a string and the datetime format is wrong for 
some versions of Glance API)

* The fake download() method ends up only exposing part of the real 
download() method, and it gets the return arguments wrong for other parts

* The fake download() method masks over the real driver's complexity in 
requiring the v2 Glance API (the locations attribute of the image) and 
thus introduces further mistakes into the unit tests that (used to) 
depend on this thing.

* The fake stub_out_image_service() completely messes up the return 
values by returning the wrong parameters

All of this is just the stubbed out fakes for the image service. All of 
the above introduced subtle bugs into the unit tests and produced a 
situation where the unit tests were erroneously passing when the RBD 
image backend patches (the ones that were reverted days after inclusion 
right before Icehouse was released, remember?) used the fake image 
service instead of actually verifying the interface that exists in the 
real driver.

If instead of writing full fake drivers for some of these important 
parts of the code base, we instead just mocked out the specific methods 
that cross performance or dependency-sensitive boundaries, I think we'd 
all be a bit saner.

Best,
-jay



More information about the OpenStack-dev mailing list