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.

115 lines
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.
<!-- more -->
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:
```mongodb
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](https://docs.python.org/dev/library/unittest.mock.html)
for both versions.
An usage example would be something like this: If I had a function like:
```python
def request():
return connector.collection.find({'field': 'value'})
```
and I want to test it, I could this:
```python
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
```python
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:
```python
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.
```python
count_mock = MagicMock(return_value=0)
mocked_find.return_value = MagicMock(
**{'count': count_mock})
```