Learning Goals
3 min- Use
setUpto build fresh state before every test method. - Use
tearDownto clean up (files, connections) after each. - Understand test isolation — why each test must start clean.
- Use
setUpClass/tearDownClassfor expensive one-time setup.
Warm-Up · Tests Must Not Bleed
5 min# ✗ shared state between tests — a hidden bug factory inv = Inventory() class TestBad(unittest.TestCase): def test_add(self): inv.add("sword") # mutates the shared inv self.assertEqual(len(inv), 1) def test_empty(self): self.assertEqual(len(inv), 0) # FAILS if test_add ran first!
Tests must be independent: the result of one must never depend on whether another ran first. setUp rebuilds fresh state before every single test method, guaranteeing isolation. Shared mutable state is one of the top causes of "works alone, fails together".
New Concept · The Lifecycle Hooks
14 minsetUp runs before each test
class TestInventory(unittest.TestCase): def setUp(self): # runs before EVERY test_ method → fresh, isolated state self.inv = Inventory() self.inv.add("sword", 2) def test_count(self): self.assertEqual(self.inv.count("sword"), 2) def test_add_more(self): self.inv.add("sword", 3) # only affects THIS test's copy self.assertEqual(self.inv.count("sword"), 5)
Both tests get their own fresh self.inv with 2 swords. test_add_more can't pollute test_count.
The execution order
for each test method:
setUp() ← build fresh state
test_xxx() ← the test
tearDown() ← clean up (even if the test failed)tearDown for cleanup
import tempfile, os class TestFileWriter(unittest.TestCase): def setUp(self): self.path = tempfile.mktemp() # a temp file path def tearDown(self): if os.path.exists(self.path): os.remove(self.path) # clean up no matter what def test_write(self): write_log(self.path, "hello") self.assertTrue(os.path.exists(self.path))
tearDown runs even if the test failed — perfect for releasing files, closing connections, deleting temp data.
Class-level hooks for expensive setup
class TestBigData(unittest.TestCase): @classmethod def setUpClass(cls): cls.data = load_huge_dataset() # ONCE for the whole class @classmethod def tearDownClass(cls): cls.data = None
setUp / tearDown → before/after EACH test (isolation) setUpClass / tearDownClass → ONCE per class (speed for costly setup)
Use class-level only for read-only or expensive resources — sharing mutable state breaks isolation again.
Worked Example · Testing a DB-Backed Class
12 min# test_tasks.py — fresh in-memory DB per test import unittest, sqlite3 from tasks import TaskStore # uses a sqlite connection class TestTaskStore(unittest.TestCase): def setUp(self): # in-memory DB → fast, isolated, auto-discarded self.con = sqlite3.connect(":memory:") self.store = TaskStore(self.con) self.store.init_schema() def tearDown(self): self.con.close() def test_add_and_list(self): self.store.add("buy roti") tasks = self.store.list_all() self.assertEqual(len(tasks), 1) self.assertEqual(tasks[0]["title"], "buy roti") def test_starts_empty(self): self.assertEqual(self.store.list_all(), []) # fresh DB every time def test_complete(self): tid = self.store.add("homework") self.store.complete(tid) self.assertTrue(self.store.get(tid)["done"]) if __name__ == "__main__": unittest.main()
Read the diff
Each test gets a brand-new in-memory SQLite database in setUp and closes it in tearDown. test_starts_empty passes reliably because no previous test could have added rows — the DB is recreated every time. Using :memory: makes this fast and leaves zero files behind. This is exactly how real database test suites stay isolated and quick.
Try It Yourself
13 minTake a TestCase where you re-create the same object in every test, and move that into setUp. Confirm the tests still pass and are now cleaner.
Write two tests that both mutate self.thing. Show that with setUp they pass in any order; remove setUp (share one object) and watch them break.
Write a TestCase that creates a temp file in setUp and removes it in tearDown. Make a test fail on purpose and confirm the file is STILL cleaned up.
Mini-Challenge · Fast vs Isolated
8 minBuild a TestCase for a class that needs an expensive resource (pretend load_huge_dataset() takes 2 seconds via time.sleep(2)). Use setUpClass to load it once, then write 3 read-only tests. Time the suite vs putting the load in setUp. Discuss the trade-off with isolation.
Show the structure + discussion
import unittest, time def load_huge_dataset(): time.sleep(2) # pretend it's slow return list(range(1000)) class TestBig(unittest.TestCase): @classmethod def setUpClass(cls): cls.data = load_huge_dataset() # 2s ONCE, not per test def test_len(self): self.assertEqual(len(self.data), 1000) def test_first(self): self.assertEqual(self.data[0], 0) def test_last(self): self.assertEqual(self.data[-1], 999)
setUpClass: 2s total. setUp: 6s (2s × 3 tests). Safe ONLY because the tests are read-only — if any test mutated cls.data, you'd lose isolation and reintroduce order-dependence.
Recap
3 minsetUp runs before every test (fresh isolated state); tearDown runs after every test (cleanup, even on failure). Tests must be independent — never share mutable state. setUpClass/tearDownClass run once per class for expensive, read-only resources. In-memory SQLite is the classic fast, isolated test DB. Next: running whole suites with discovery.
Vocabulary Card
- setUp / tearDown
- Hooks run before / after each test method.
- test isolation
- Each test runs independently of the others' state and order.
- setUpClass / tearDownClass
- Hooks run once per test class — for costly, read-only setup.
- fixture
- The prepared state/data a test needs to run.
Homework
4 minWrite a TestCase for a class that needs setup (a store, a parser with config, a game with a board). Use setUp for fresh state and tearDown for any cleanup. Prove isolation by writing two tests that mutate state and pass in either order.
Model on test_tasks.py with the in-memory DB pattern, or use a fresh game board built in setUp.