Skip to Content
Course content

294: Test Fixtures with setUp and tearDown

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a piece of code that looks perfectly reasonable at first glance. Imagine you're writing tests for a class that handles temporary configuration files. You want to make sure each test starts with a clean file and cleans up after itself so you don't leave junk all over your hard drive.

import unittest
import os

class ConfigManager:
    def write_config(self, filename, data):
        with open(filename, 'w') as f:
            f.write(data)

class TestConfigManager(unittest.TestCase):
    def test_write_config_creates_file(self):
        manager = ConfigManager()
        filename = "test_config.txt"
        manager.write_config(filename, "key=value")
        self.assertTrue(os.path.exists(filename))
        os.remove(filename)

    def test_write_config_content(self):
        manager = ConfigManager()
        filename = "test_config.txt"
        manager.write_config(filename, "user=admin")
        with open(filename, 'r') as f:
            self.assertEqual(f.read(), "user=admin")
        os.remove(filename)

The Problem with Manual Cleanup

This works... until it doesn't. Specifically, it works as long as your tests pass. But watch what happens if you change that second assertion to self.assertEqual(f.read(), "wrong_value"). The test fails, an exception is raised, and the execution of that method stops immediately. The line os.remove(filename) is never reached.

Now you've got a "leaked" file. In a small project, this is a nuisance. In a large CI/CD pipeline with thousands of tests, this is a nightmare. You'll start seeing "flaky" tests where a test fails not because the code is broken, but because a previous failed test left a file behind that's now interfering with the current run. I've spent way too many hours debugging "ghost failures" caused by exactly this.

Automating the Lifecycle with setUp and tearDown

This is where setUp() and tearDown() come in. These are special methods provided by unittest.TestCase. Think of setUp as the "pre-game" routine and tearDown as the "cleanup crew."

  • setUp(): Runs automatically before every single test method in the class.
  • tearDown(): Runs automatically after every single test method, regardless of whether the test passed, failed, or crashed.

By moving the shared logic here, we remove the duplication and guarantee that the cleanup happens no matter what. Let's refactor the previous example:

import unittest
import os

class ConfigManager:
    def write_config(self, filename, data):
        with open(filename, 'w') as f:
            f.write(data)

class TestConfigManager(unittest.TestCase):
    def setUp(self):
        # This runs before every test
        self.manager = ConfigManager()
        self.filename = "test_config.txt"

    def tearDown(self):
        # This runs after every test, even if it fails
        if os.path.exists(self.filename):
            os.remove(self.filename)

    def test_write_config_creates_file(self):
        self.manager.write_config(self.filename, "key=value")
        self.assertTrue(os.path.exists(self.filename))

    def test_write_config_content(self):
        self.manager.write_config(self.filename, "user=admin")
        with open(self.filename, 'r') as f:
            self.assertEqual(f.read(), "user=admin")

Why This Approach Wins

Notice how much leaner the actual test methods are now. They only contain the logic specific to what they are testing. I generally follow a rule: if I'm writing the same three lines of initialization in more than two tests, it's time to move them into setUp.

One thing to keep in mind: setUp and tearDown create a "fresh" state for every test. This is intentional. You never want Test A to depend on something Test B did. Total isolation is the gold standard for testing. If you find yourself needing to set up something incredibly expensive (like a massive database connection) that takes seconds to start, there are other tools like setUpClass, but for 90% of your work, the standard setUp and tearDown are exactly what you need.




📋 Practical Task

Exercise: Isolating a Shopping Cart Database with Test Fixtures

You are testing a ShoppingCart class that uses a simple dictionary to simulate a database of products. Currently, the tests are manually initializing the cart, and if a test fails, the "database" state isn't being cleared properly between runs.

Your Task: Refactor the following code to use setUp() and tearDown(). Move the cart initialization and the "database" clearing logic into the appropriate fixture methods so that each test starts with a clean slate.

import unittest

class ShoppingCart:
    def __init__(self, db):
        self.db = db
        self.items = []

    def add_item(self, item_id):
        if item_id in self.db:
            self.items.append(self.db[item_id])
        else:
            raise ValueError("Item not found in database")

class TestShoppingCart(unittest.TestCase):
    def test_add_valid_item(self):
        # Manual setup
        db = {"A1": "Apple", "B2": "Banana"}
        cart = ShoppingCart(db)
        
        cart.add_item("A1")
        self.assertEqual(len(cart.items), 1)
        self.assertEqual(cart.items[0], "Apple")
        
        # Manual cleanup
        db.clear()

    def test_add_invalid_item(self):
        # Manual setup
        db = {"A1": "Apple", "B2": "Banana"}
        cart = ShoppingCart(db)
        
        with self.assertRaises(ValueError):
            cart.add_item("Z9")
        
        # Manual cleanup
        db.clear()

if __name__ == "__main__":
    unittest.main()
Rating
0 0

There are no comments for now.

to be the first to leave a comment.