This post follows on from a previous post, available here. I’d recommend giving it a read as we’re going to be building on that knowledge of Python’s unit test framework.
In my previous post I gave examples of how to write simple unit tests and while that’s fine when your functions are that simple what about more complex code? Consider the following example;
import file_utils | |
file_path = "INSERT_PATH_HERE" | |
def get_users(): | |
users = file_utils.read_file(file_path) | |
return users | |
def save_users(users): | |
file_utils.write_file(file_path, users) |
Here, users.py has a dependency on file_utils, but we don’t want to test file_utils because it should have its own tests, we also don’t want to have to make our users.py tests cater for what file_utils requires. What we really need is a way to replace file_utils, with something we can control for the sake of our test, in such a way that it doesn’t require any changes to users.py. This is where we would use “unittest.mock“.
unittest.mock allows you to replace the portions of code you don’t need to test with mocks, these mocks allow you to perform assertions to check they were called with the right arguments and the right number of times. In our case we can use unittest.mock to replace file_utils functions but still verify users.py is calling them as we expect.
Before I proceed, it’s worth noting that there is a back port of mock available for Python 2.7 for you to ‘pip install’. Just make sure you validate you’re grabbing the right thing!
So let’s take a look at get_users.
def get_users(): users = file_utils.read_file(file_path) return users
We need to;
- Replace file_utils.read_file(file_path) with a mock, so we don’t have to call the actual thing.
- Have our mock provide a return value that we control so we can assert get_users is passing back the right thing.
- Validate that we do call read_file as part of get_users.
Nothing too difficult! First the mock;
>>> import file_utils >>> import unittest >>> import mock >>> file_utils.read_file = mock.MagicMock(name="read_file") >>> print file_utils.read_file
Here we’ve replaced read_file with our mock, this mock records how it’s used and will allow us to perform assertions later. To make this mock return a value, it’s a simple case of passing an extra argument in.
>>> file_utils.read_file = mock.MagicMock(name="read_file", return_value="user")
Adding a return_value lets you have your mock provide a controlled response.
Finally we need to validate that our function calls file_utils.read_file with the arguments we expect. As the mock records how it’s used it’s easy to make assertions;
file_utils.read_file.assert_called_once_with("FILEPATH")
There’s a number of assertions we can make, but within our test we just want to make sure we call “read_file” once, and with a file path.
Here’s the test in its entirety as well as a test for save_users;
import unittest | |
import mock | |
import users | |
import file_utils | |
class UsersTest(unittest.TestCase): | |
_file_path = "SOME FILE PATH" | |
def test_get_users(self): | |
user = "user1" | |
# create a mock for read_file which returns the value user | |
file_utils.read_file = mock.MagicMock(name="read_file", return_value=user) | |
result = users.get_users() | |
self.assertEqual(user, result) | |
file_utils.read_file.assert_called_once_with(self._file_path) | |
def test_save_users(self): | |
# create a simple mock for write file | |
file_utils.write_file = mock.MagicMock(name="write_file") | |
test_users = "user1" | |
users.save_users(test_users) | |
file_utils.write_file.assert_called_with(self._file_path, test_users) | |
test_users = "user1, user2" | |
users.save_users(test_users) | |
file_utils.write_file.assert_called_with(self._file_path, test_users) | |
if __name__ == '__main__': | |
unittest.main() |
For test_save_users, we’ve actually gone a step further and made two calls to save_users. As I’ve used the same mock throughout the test, I can’t use assert_called_once_with because we’re genuinely calling it twice! That’s why I’ve swapped it for assert_called_with instead.
So that’s how to create mocks, wasn’t too painful was it? That’s not all there is to unittest.mock and in future posts we will mock file_utils and it’s file operations, we will also go into some of the cool features unittest.mock provides such as having your mocks throw exceptions!
2 thoughts on “Unit Testing with Python (Part 2) – Mocking”