In complex projects, unit tests are of great importance. This is why code quality and maintainability of the unit tests are important.
***** You can follow me on LinkedIn *****
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()]