The source content for blog.juliobiason.me
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

4.4 KiB

+++ title = "Mocking A Mock" date = 2016-07-21 category = "code"

[taxonomies] tags = ["python", "mock", "mongodb", "find", "count", "en-au"] +++

Mocks are an important part of testing, but learn how to properly mock stuff.

A few weeks ago we had a test failing. Now, tests failing is not something worth a blog post, but the solution -- and the reason it was failing -- is.

A few background information first: The test is part of our Django project; this project stores part of the information on MongoDB, because the data is schemaless -- it comes from different sources and each source has its own format. Because MongoDB is external to our project, it had to be mocked (sidenote: mocks are there exactly to do this: the avoid having to manage something external to your project).

PyMongo, the MongoDB driver for Python, has a find() function, pretty much like the MongoDB API; this function returns a list (or iterator, I guess) with all the result records in the collection. Because it is a list (iterator, whatever), it has a count() function that returns the number of records. So you have something like this:

connector.collection.find({'field': 'value'}).count()

(Find everything which has a field named "field" that has a value of "value" and count the results. Pretty simple, right?)

The second hand of information you need is about the mock module. Python 3 has a module for mocking external resources, which is also available to Python 2. The interface is the same, so you can refer to the Python 3 documentation for both versions.

An usage example would be something like this: If I had a function like:

def request():
  return connector.collection.find({'field': 'value'})

and I want to test it, I could this:

class TestRequest(unittest.TestCase):
   @patch("MyModule.connector.collection.find")
   def test_request(self, mocked_find):
      mocked_find.return_value = [{'field': 'value', 'record': 1},
                                  {'field': 'value', 'record': 2}]
      result = request()
      self.assertDictEqual(result, mocked_find.return_value)

Kinda sketchy for a test, but I just want to use to explain what is going on: the @patch decorator is creating a stub for any call for MyModule.connector.collection.find; inside the test itself, the stub is being converted to a mock by setting a return_value; when the test is run, the mock library will intercept a call to the collection.find inside MyModule.connector (because that module imported PyMongo driver to its namespace as connector) and return the return_value instead.

Simple when someone explains like this, right? Well, at least I hope you got the basics of this mocked stuff.

Now, what if you had to count the number of results? It's pretty damn easy to realize how to do so: just call count() on the resulting list, or make it return an object that has a count() property.

The whole problem we had was that the result of find() was irrelevant and all we wanted was the count. Something like

def has_values():
   elements = connector.collection.find({'field': 'value'}).count()
   return elements > 1

First of all, you can't patch MyModule.connector.collection.find.count because you'll only stub the count call, not find, which will actually try to connect on MongoDB; so the original patch is required. And you can't patch both find and count because the first patch will return a new MagicMock object, which will not be patched (after all, it is another object). The original developer tried to fix it this way:

mocked_find.count.return_value = 0

... which, again, doesn't work because the call to find() will return a MagicMock that doesn't have its count patched. But the developer never realized that because MagicMock tries its best to not blow up your tests, including having return values to conversions like... int. And it will always return 1.

Is your head spinning yet? Mine sure did when I realized the whole mess it was being made. And let me repeat this: The problem was not that MongoDB was being mocked, but that it was being mocked in the wrong way.

The solution? As pointed above, make find return an object with a count method.

count_mock = MagicMock(return_value=0)
mocked_find.return_value = MagicMock(
    **{'count': count_mock})