9.4. Isolate Tests from Each Other with setUp, tearDown, setUpModule, and tearDownModule

TestCase classes (see Item 76: “Verify Related Behaviors in TestCase Subclasses”) often need to have the test environment set up before test methods can be run; this is sometimes called the test harness. To do this, you can override the setUp and tearDown methods of a TestCase subclass. These methods are called before and after each test method, respectively, so you can ensure that each test runs in isolation, which is an important best practice of proper testing.

For example, here I define a TestCase that creates a temporary directory before each test and deletes its contents after each test finishes:

# environment_test.py from pathlib import Path from tempfile import TemporaryDirectory from unittest import TestCase, main

class EnvironmentTest(TestCase):
def setUp(self):

self.test_dir = TemporaryDirectory() self.test_path = Path(self.test_dir.name)

def tearDown(self):

self.test_dir.cleanup()

def test_modify_file(self):
with open(self.test_path / 'data.bin', 'w') as f:

...

if __name__ == '__main__':

main()

When programs get complicated, you’ll want additional tests to verify the end-to-end interactions between your modules instead of only testing code in isolation (using tools like mocks; see Item 78: “Use Mocks to Test Code with Complex Dependencies”). This is the difference between unit tests and integration tests. In Python, it’s important to write both types of tests for exactly the same reason: You have no guarantee that your modules will actually work together unless you prove it.

One common problem is that setting up your test environment for integration tests can be computationally expensive and may require a lot of wall-clock time. For example, you might need to start a database process and wait for it to finish loading indexes before you can run your integration tests. This type of latency makes it impractical to do test preparation and cleanup for every test in the TestCase class’s setUp and tearDown methods.

To handle this situation, the unittest module also supports module-level test harness initialization. You can configure expensive resources a single time, and then have all TestCase classes and their test methods run without repeating that initialization. Later, when all tests in the module are finished, the test harness can be torn down a single time. Here, I take advantage of this behavior by defining setUpModule and tearDownModule functions within the module containing the TestCase classes:

# integration_test.py from unittest import TestCase, main

def setUpModule():

print('* Module setup')

def tearDownModule():

print('* Module clean-up')

class IntegrationTest(TestCase):
def setUp(self):

print('* Test setup')

def tearDown(self):

print('* Test clean-up')

def test_end_to_end1(self):

print('* Test 1')

def test_end_to_end2(self):

print('* Test 2')

if __name__ == '__main__':

main()

$ python3 integration_test.py * Module setup * Test setup * Test 1 * Test clean-up .* Test setup * Test 2 * Test clean-up .* Module clean-up

OK

I can clearly see that setUpModule is run by unittest only once, and it happens before any setUp methods are called. Similarly, tearDownModule happens after the tearDown method is called.

9.4.1. Things to Remember

✦ It’s important to write both unit tests (for isolated functionality) and integration tests (for modules that interact with each other).

✦ Use the setUp and tearDown methods to make sure your tests are isolated from each other and have a clean test environment.

✦ For integration tests, use the setUpModule and tearDownModule module-level functions to manage any test harnesses you need for the entire lifetime of a test module and all of the TestCase classes that it contains.