5 Tips to Improve Your Unit Tests in Python

The unit tests have great importance in complex projects. Hence, code quality and maintainability of the unit tests are important.

Photo by Nick Fewings on Unsplash

1) @patch.object is your friend

There are some difficulties of the @patch decorator:

The paths of the objects can be confusing in big Python projects. Sometimes, resolving the paths can make difficulties for the unit tests.

You need to write the path in the @patch decorator as a string. If you write the path wrong, then most probably, your IDE will not warn you about it.

You must write the full path of the mock object for each test function (a.k.a. test case).

You can import the objects easily via @patch.object.

Example:

from unittest.mock import patch@patch("folder1.folder2.file1.class1.func1")
def test_example(self, func1):
pass
@patch("folder1.folder2.file1.class1.func1")
def test_example_2(self, func1):
pass

vs.:

from unittest.mock import patch
from folder1.folder2.file1 import class1
@patch.object(class1, "func1")
def test_example(self, func1):
pass
@patch.object(class1, "func1")
def test_example_2(self, func1):
pass

2) Avoid using global variables

Using global variables in the unit tests may end up with the wrong test results.

If one of your unit test functions modifies the global variable, then your other test functions may end up with the wrong test results.

Let’s assume you have two functions:

def remove_from_list(name_list, name):
name_list.remove(name)
return name_list


def count_list(name_list):
return len(name_list)

You will have two unit tests:

names = ["Michael", "John"]class ExampleTests(TestCase):

def test_remove_from_list(self):
self.assertEqual(remove_from_list(names, "Michael"), ["John"])

def test_count_list(self):
self.assertEqual(count_list(names), 2)

At first glance, they look fine. However, the second test will be failed:

FAILED tests/test_example.py::ExampleTests::test_count_list - AssertionError: 1 != 2

Because the remove_from_list function modifies the names variable.

Let’s put the names list in a getter function (see: mutator method):

def get_names():
return ["Michael", "John"]


class ExampleTests(TestCase):

def test_remove_from_list(self):
self.assertEqual(remove_from_list(get_names(), "Michael"), ["John"])

def test_count_list(self):
self.assertEqual(count_list(get_names()), 2)

Both of the tests are passed.

I could have created the shallow copy of the names variable:

# from copy import copydef test_remove_from_list(self):
self.assertEqual(remove_from_list(copy(names), "Michael"), ["John"])

It is not a recommended way to pass the variable to another test function. Because you will have to remember to use the copy function when you need to use the names list.

3) Get rid of unnecessary mock variables

You must create a function argument when you create a mock object.

Example:

@patch.object(class1, "func1", return_value=["abc"])
def test_example(self, mock_func1):
pass

If you don’t need the mock_func1 variable in the test function, then you can envelop the mock object like this:

@patch.object(class1, "func1", Mock(return_value=["abc"]))
def test_example(self):
pass

4) @patch.multiple is your friend

Let’s assume you will need to create a mock object for two different methods of one class. @patch.multiple is for that.

Example:

@patch.multiple(
class1,
get_name=Mock(return_value="Michael"),
get_age=Mock(return_value=25),
)
def test_example(self):
self.assertEqual("Michael", class1().get_name())
self.assertEqual(25, class1().get_age())

5) Faker is also your friend

Faker is a Python package that generates fake data for you. In my previous test functions, I ran my test functions with the same strings (Michael and John).

I could have run the test functions with random names every time thanks to Faker.

Example:

names = [faker.name(), faker.name()]