5 Tips to Improve Your Unit Tests in Python

In complex projects, unit tests are of great importance. This is why code quality and maintainability of the unit tests are important.

Photo by Nick Fewings on Unsplash

1) @patch.object is your friend

The @patch decorator has some difficulties:

The paths of the objects can be confusing in large Python projects. Sometimes resolving paths can create difficulties for unit tests.

You have to write the path in the @patch decorator as a string. If you mistype the path, your IDE probably won’t warn you about it.

For each test function (a.k.a. test case) you must write the full path to the mock object.

You can easily import objects 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 unit tests may result in incorrect test results.

If one of your unit test functions changes the global variable, your other test functions may result in incorrect test results.

Suppose 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 good. However, the second test will fail:

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

Because the remove_from_list function changes the names variable.

Let’s put the names list into 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 tests passed.

I could have created a 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 when you need to use the names list you will have to remember to use the copy function

3) Get rid of unnecessary mock variables

When you create a mock object you must create a function argument

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, you can wrap the mock object like this:

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

4) @patch.multiple is your friend

Suppose you will need to create a mock object for two different methods of a class. That’s what @patch.multiple is for

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).

Thanks to Faker, I could run test functions with random names every time

Example:

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

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store