Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
294: Test Fixtures with setUp and tearDown
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()
There are no comments for now.