Tutorial 2: Write Your First Unit Test
- Contributor
- Mar 22
- 3 min read
Time to test real code. This tutorial walks through writing a unit test that catches a real bug in a real function.
What You'll Build
A unit test for a small function in your codebase. You'll use the Arrange-Act-Assert structure.
Step 1: Pick a Function (5 min)
Look in your codebase for a function with:
Clear input → output behavior
Minimal side effects (no DB calls, no HTTP)
Some logic (validation, calculation, transformation)
Good candidates:
A discount calculator
A validator
A date formatter
A string parser
Bad candidates for your first test:
Functions that hit the database (those are integration tests)
Functions that make API calls
UI components (different patterns)
Step 2: Read the Function (5 min)
Understand what it does. What are the inputs? What are the outputs? What edge cases might exist?
Sample function:
def calculate_discount(price, customer_tier):
"""Calculate discount based on customer tier."""
if customer_tier == "premium":
return price * 0.10
elif customer_tier == "gold":
return price * 0.20
return 0
Step 3: Write the First Test (5 min)
Start with the happy path. One test for the most common case.
def test_premium_customer_gets_10_percent_discount():
# Arrange
price = 100
tier = "premium"
# Act
discount = calculate_discount(price, tier)
# Assert
assert discount == 10
Notice the three sections. The test name describes the behavior.
Step 4: Run It (1 min)
pytest tests/test_discount.py
Should pass. If not, either the test is wrong or the function is wrong. Both teach you something.
Step 5: Add More Cases (10 min)
def test_gold_customer_gets_20_percent_discount():
discount = calculate_discount(100, "gold")
assert discount == 20
def test_standard_customer_gets_no_discount():
discount = calculate_discount(100, "standard")
assert discount == 0
def test_unknown_tier_gets_no_discount():
discount = calculate_discount(100, "platinum")
assert discount == 0
Each test covers one specific behavior.
Step 6: Look for Edge Cases (10 min)
Ask "what if?":
What if price is 0?
What if price is negative?
What if tier is None?
What if price is huge?
Write tests for each:
def test_zero_price():
discount = calculate_discount(0, "premium")
assert discount == 0
def test_negative_price():
discount = calculate_discount(-100, "premium")
# What's the expected behavior?
# Check what the function does. If it's wrong, fix the function.
The negative price case might reveal a bug. If the function returns -10 for a negative price, that's probably wrong — discounts shouldn't be negative.
This is the moment when tests catch real bugs.
Step 7: Fix Any Bug You Find (varies)
If your test failed against the existing code (or revealed unexpected behavior), update the function:
def calculate_discount(price, customer_tier):
if price <= 0:
return 0
if customer_tier == "premium":
return price * 0.10
elif customer_tier == "gold":
return price * 0.20
return 0
Re-run tests. They should all pass now.
Step 8: Refactor With Confidence (5 min)
Now the tests are in place, you can refactor the function safely:
DISCOUNT_RATES = {
"premium": 0.10,
"gold": 0.20,
}
def calculate_discount(price, customer_tier):
if price <= 0:
return 0
rate = DISCOUNT_RATES.get(customer_tier, 0)
return price * rate
Run tests. All still pass. You changed the implementation; behavior is preserved. That's the magic of tests.
What You Just Did
You wrote tests that:
Documented the function's expected behavior
Caught a real bug (the negative-price edge case)
Enabled a refactor with confidence
This is what unit tests buy you. The investment per test is small. The compound value is large.
Common Failure Modes
Testing the implementation, not the behavior. Tests that break when you refactor the function legitimately. Test what the function should do, not how it does it.
Vague assertions. assert discount > 0 passes for many wrong values. Be specific.
One-test-per-function thinking. A function with branches needs multiple tests. Cover the branches.
Skipping edge cases. Happy path only. Most bugs live in edges.
Tests that test the test. Setting up a mock and asserting the mock was called as expected. You're testing the mock, not the code.
Next Tutorial
Unit tests are powerful but miss integration bugs. Next: Tutorial 3: Write a Useful Integration Test.
Related reading
Keep learning. This article is part of the Software Testing Foundations path in the ShiftQuality Learning Center. Learn to design tests that catch real bugs.


