Unit testing basics with Python
Libs used: pytest and mock
The mock lib provides a function called patch
, used as a decorator to mock the
things we are going to import in our to-be-tested function. In this example,
CouchDB is being used and we are going to our application’s save method. Our
function imports CouchDB from cloudant, but we don’t want to use the real
CouchDB object. The reason is that we want to make sure all the results are
coming from our program, not some third-party library.
Example of our dummy program:
from cloudant import CouchDB
class Connector:
def __init__(self):
self.couchdb = CouchDB(...)
def save(document):
couchdb.bulk_docs([document])
To test the save method, the CouchDB class should be mocked, which is the class
that is imported inside the Connector module. The patch decorator is useful for
that. I’m assuming the Connector lives in a dummy path: databases.couchdb
.
from mock import patch
@patch('database.couchdb.CouchDB')
def test_save(mocked_couchdb):
...
With that, the return value of the CouchDB class objects are all
Mock()
objects. So everytime the self.couchdb = CouchDB(...)
is executed,
self.couchdb
receives a Mock()
object. And, of course, we can fiddle with
the return value, it can be basically whatever we want. We could change it
using a return_value
parameter in the path or setting
mocked_couchdb.return_value = ...
.
We can also use the side_effect
parameter, which is basically a list
containing the sequence of return values that are attributed to the object as
the test cases run. And, lastly, speaking of test cases, to avoid calling the
save
method several times with different documents to test every possible
case (and, frankly, turning the test basically unreadable), we can use the
parametrize decorator.
It has two parameters, a tuple containing the names of the test cases and a list
containing tuples with the test cases. Going on with the database save example,
we can store the bulk_docs
(it’s a real method from cloudant’s CouchDB)
responses in a variable and then assert that the response is actually what it’s
supposed to be, that’s what tests are for, right? So:
import pytest
from mock import patch
@patch('database.couchdb.CouchDB')
@pytest.mark.parametrize(
('document', 'expected_response'), [({'_id': '1'}, dummy_return), (...)])
def test_save(mocked_couchdb, document, expected_response):
connector = Connector()
response = connector.save(document)
assert response == expected_return
Breaking it down a little bit: the tuple ('document', 'expected_response')
represents what we are providing as test cases, and in the cases array, in the
first tuple: ({'_id': '1'}, dummy_return
), {'_id': '1'}
is the document,
while dummy_return
is the expected return.
We can add as many test cases as we want in the array of tests, e.g.:
({'_id': '2'}, dummy_return2)
, ({'_id': '3'}, False)
, etc.
The test will run for every case we have defined in the parametrize. Therefore, we asserted that given an input, it returned the correct response. And this covers the basic stuff of testing with pytest and mock, how to use the return values and the side effects. It should be enough for most applications.
Don’t forget to check the documentations as well. There are plenty of other things there.