Skip to Content
Course content

304: Test-Driven Development Basics in Python

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

I'll be honest: early in my career, I thought writing tests was a chore that slowed me down. I'd write a whole feature, run it once, see it work for the most obvious case, and call it a day. Then I'd push the code, and an hour later, a bug report would come in because someone entered a negative number or a null value that I hadn't even considered. That's how I learned the hard way that "it seems to work" isn't a strategy.

Let's look at a piece of code that looks perfectly fine at first glance. Imagine we're building a simple discount system for an e-commerce site.

def apply_discount(price, discount_percent):
    return price * (1 - discount_percent / 100)

# Testing it manually
print(apply_discount(100, 20))  # 80.0 - Works!
print(apply_discount(50, 10))   # 45.0 - Works!

The Danger of the Happy Path

The code above works for what I call the "happy path"—the scenario where the user does exactly what we expect. But what happens if a marketing manager accidentally sets a discount to 110% during a flash sale? Or what if the price is passed as a negative number by mistake?

print(apply_discount(100, 110)) # -10.0

Suddenly, our store is paying customers to take products. In a real production environment, this is the kind of bug that gets you a very unpleasant phone call at 2:00 AM. We could just add an if statement now to fix it, but that's how we end up with "spaghetti code"—adding patches on top of patches without a clear plan.

Flipping the Script with Red-Green-Refactor

This is where Test-Driven Development (TDD) comes in. Instead of writing the code and then trying to "break" it, we write the test first. We define what "correct" looks like before we even write the logic. TDD follows a rhythmic cycle: Red, Green, Refactor.

  • Red: Write a test for a small bit of functionality and watch it fail.
  • Green: Write the bare minimum amount of code to make that test pass.
  • Refactor: Clean up the code while ensuring the tests still pass.

I prefer using pytest because it's less boilerplate than the built-in unittest module. Let's apply this to our discount bug. First, we write a test that describes the behavior we actually want: the price should never drop below zero.

# test_discount.py
from discount_logic import apply_discount

def test_discount_cannot_be_negative():
    # If the discount is 110%, the price should be 0, not -10
    assert apply_discount(100, 110) == 0

If we run this right now, it fails. That's our Red phase. Now, we write just enough code in discount_logic.py to make it pass.

Tightening the Logic with pytest

Instead of over-engineering a complex validation system, we'll use max() to ensure we never return a value lower than zero. This is the Green phase.

# discount_logic.py
def apply_discount(price, discount_percent):
    final_price = price * (1 - discount_percent / 100)
    return max(0, final_price)

Run the test again. It passes. Now, we don't stop there. We might realize that a negative price input is also possible. So, we write another test: test_negative_price_input(). It fails. We update the code to handle it. We repeat this until we've covered all the edge cases we can think of.

The beauty of this approach is that you're not guessing if your fix broke something else. If you change the logic six months from now to add a "maximum discount cap," you just run your suite of tests. If they all stay green, you can sleep soundly knowing you haven't reintroduced that "paying the customer" bug.




📋 Practical Task

Build a TDD-Based Password Strength Validator

Your task is to create a password validator using the TDD process. You must write the tests before the implementation code.

Requirements: The validator function is_strong_password(password) should return True only if:

  • The password is at least 8 characters long.
  • It contains at least one digit.
  • It contains at least one special character (e.g., !, @, #, $, %).

Instructions:

  1. Create a file named test_validator.py. Write three separate tests: one for length, one for digits, and one for special characters. Run them and confirm they all fail (since the function doesn't exist yet).
  2. Create validator.py and implement is_strong_password() using the minimum logic required to make those tests pass.
  3. Add a "Happy Path" test case where a password meets all criteria and ensure it returns True.
  4. Run your tests one last time to ensure everything is green.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.