As you’ve seen when setting up various test scenarios, sometimes you need to duplicate code to create a test context or part of your scenario. However, duplicating code for each test that you create is pretty time-consuming. Not only do you have to keep repeating yourself, it also makes it harder to maintain your tests afterwards.
I’d really like to be able to declare variables and reuse them in my functions to test class methods, or use identical lines of code for several tests without having to copy all of the lines.
Can you think of a way of doing this?
I’ll leave you to think about it…
Have you had any bright ideas?
What if we created a class for our tests? Then we could declare class attributes and methods, using them whenever we need to. This will also enable us to differentiate between our tests.
Why use a test class?
To:
group test classes together.
define a fixed environment for running test methods.
eliminate code duplication when code is common to all test cases.
make it easier to refactor tests.
Create a Test Class
It might sound complicated, but it’s actually quite simple. First of all, you need to create a new class in a test file. This test class will be given the name of the class we’re testing, with a prefix of Test
. This prefix allows us to identify it as a test class. You can then create each test as a method of this class.
Here’s an example of a test class with two tests:
class TestClass:
def test_one(self):
assert 1 == 1
def test_two(self):
assert 2 == 2
Now let’s run the pytest
command to execute our tests. We get the following result on the terminal:
You can use option -v
after the pytest
command to find out which file and class the test belongs to.
You can see in the screenshot above that there are indeed two tests. And in the details, we can also see that the tests belong to the test_class.py
file and the TestClass
test class. It’s a really useful command!
I’m now going to deliberately raise an assertion in the second test to show you how to debug a test when using a test class. It’s almost the same as what we saw in the Go Further With Pytest chapter. Here’s the same class with an error in the second test:
class TestClass:
def test_one(self):
assert 1 == 1
def test_two(self):
assert 2 == 1
Let’s see what the terminal will display after running the tests:
Surprise surprise, there’s red everywhere! Obviously, we introduced an error into the second test, and we can see from the terminal display that the test called test_two
from the TestClass
class has failed. However, it does indicate the assertion that was raised. You can now easily correct the error and re-run the tests to check that the correction has worked.
Execute Actions Before or After Tests
I’ve saved the best till last! You now know that one of the most long-winded aspects when testing is setting up the test context and environment. This is why I’m going to show you in this part of the chapter how you can make your life easier when creating tests.
May I present the setup()
and teardown()
methods!
Pytest enables you to execute instructions before or after each unit test. This feature in our test library is elegantly named setup and teardown.
On the one hand, we have a test method called setup()
, which is invoked before a test is run. You can also configure your test context and environment within this method, to populate a database or to register a user and log them in, for example.
On the other hand, we have a second method called teardown()
, which is invoked at the end of a test. You can use this method to clean up the objects you used for your testing.
There are a number of invocation levels: before each unit test, when creating a class or when importing the Pytest module. We’re going to see the setup()
and teardown()
functions being applied at several levels:
Module level
Class level
Method level
Function level
Setup/Teardown at Module Level
At module level, there are two functions:
setup_module()
, called at the start of a module.teardown_module()
, called at the end of a module.
These two functions enable us to configure the environment at a global level, applying to all tests and classes within the module. You can also add your configuration within the two function blocks. Note that these two functions will be called once for all tests.
def setup_module(module):
""" function called when module is imported """
def teardown_module(module):
""" function called after execution of all tests within a module """
Setup/Teardown at Class Level
This time, the setup_class()
method is called when the class is instantiated and the teardown_class()
method is called when the class is destroyed. The context configuration only affects the scenarios within this particular test class. It’s important to note that these methods are only invoked once for all the tests within a class.
@classmethod
def setup_class(cls):
""" function called when class is created """
@classmethod
def teardown_class(cls):
""" function called when class is destroyed """
Setup/Teardown at Method Level
Now, here we have the most useful methods: setup_method()
and teardown_method()
. The methods described previously enabled us to configure an environment to apply to several tests. However, this practice can be dangerous.
I imagine you want to know why. Quite simply, when using the previous methods, our tests become interdependent. As a result, a test might change the test environment, because every test uses the same configuration and potentially the same database. Any change in the sequence of tests could result in a global error.
It is good practice to create a setup()
and teardown()
method each time a scenario is run. For this reason, we highly recommend that you use setup and teardown at the method level.
def setup_method(self, method):
""" function called when running a unit test that is part of a class """
def teardown_method(self, method):
""" function called at the end of a unit test that is part of a class """
Setup/Teardown at Function Level
Methods for functions aren’t particularly useful for test classes, but it’s good to know about them if you’re not using a test class in your scenarios. The setup_function()
and teardown_function()
methods perform the same role as the previous two methods for class methods, but this time they apply to functions.
def setup_function(function):
""" function called when running a unit test in a function """
def teardown_function(function):
""" function called at the end of a unit test in a function """
Let’s now have a look at some code to illustrate how various setup and teardown methods are called using some print instructions. Let’s take the following code and look at the print output on the terminal. You’ll see the order in which each function is called.
def setup_module(module):
print("\n--> Setup module")
def teardown_module(module):
print("--> Teardown module")
class TestClass:
@classmethod
def setup_class(cls):
print("--> Setup class")
@classmethod
def teardown_class(cls):
print("--> Teardown class")
def setup_method(self, method):
print("--> Setup method")
def teardown_method(self, method):
print("\n--> Teardown method")
def test_one(self):
print("--> Run first test")
def test_two(self):
print("--> Run second test")
Here’s the terminal output we see after running the following command, pytest -s
:
As you can see in the above screenshot, you can confirm that the methods are working correctly and are run in a specific order. Go ahead and play around with this code to understand the different setup()
and teardown()
methods.
Over to You!
Go back to the OC-commerce project using the Django framework.
Your mission:
Organize your testing into classes.
You’ll see that managing contexts and databases becomes a lot easier when you use classes.
Find a suggested solution on GitHub!
Let’s Recap!
Using test classes enables you to organize your project testing.
Test classes allow you to create methods to minimize code duplication.
You need to name your test with a prefix of
test_
or a suffix of_test
, otherwise Pytest won’t run it.The
setup()
andteardown()
methods allow you to manage the context at the beginning and end of a test and these methods operate at a number of levels:Module level
Class level
Method level
Function level
In this chapter, we’ve been through all of the concepts that relate to unit testing. In the next chapter, we’re going to cover one final point that will enable you to verify and assess the quality of our testing, to see whether or not we did a good job!