Skip to Content
Course content

293: Writing Unit Tests with unittest

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

Imagine you're building a high-performance engine. You wouldn't just bolt every single part together, pour in the oil, and turn the key for the first time, hoping it doesn't explode. Instead, you'd test the fuel pump separately. You'd test the spark plugs. You'd verify the pistons move smoothly before they ever touch the crankshaft. If the engine fails to start later, you don't have to guess where the problem is; you already know the fuel pump works because you tested it in isolation.

That's exactly what unit testing is. In Python, the unittest module is your workbench. It allows you to isolate a tiny piece of logic—a single function or method—and prove it works exactly as expected before you plug it into the rest of your application.

Building Your Test Rig

To use unittest, you don't just write loose functions. You create a class that inherits from unittest.TestCase. This gives you access to a suite of "assertion" methods that act as your quality checks. I've found that the biggest mistake beginners make is forgetting that any method you want the test runner to execute must start with the word test. If you name it check_total, Python will just ignore it. Name it test_total, and it's officially part of the suite.

import unittest

# The code we are testing
def calculate_discount(price, discount_percent):
    if not (0 <= discount_percent <= 100):
        raise ValueError("Discount must be between 0 and 100")
    return price * (1 - (discount_percent / 100))

# The Test Rig
class TestDiscountLogic(unittest.TestCase):
    def test_standard_discount(self):
        self.assertEqual(calculate_discount(100, 20), 80)
        self.assertEqual(calculate_discount(50, 10), 45)

    def test_zero_discount(self):
        self.assertEqual(calculate_discount(100, 0), 100)

Checking Your Assumptions

In the example above, assertEqual is the heavy lifter. It's basically saying: "I expect the result of this function to be exactly 80. If it isn't, stop everything and tell me why." But real-world code isn't always about getting the right number; sometimes it's about failing correctly.

If your function is supposed to throw an error when given bad input, you can't use assertEqual because the code will crash before the assertion happens. Instead, we use assertRaises. I use this constantly when validating user input or API responses to make sure my "guard rails" are actually working.

    def test_invalid_discount_raises_error(self):
        with self.assertRaises(ValueError):
            calculate_discount(100, 110) # 110% is impossible

Avoiding Repetitive Setup

As your tests grow, you'll notice you're doing the same boring setup in every single test method—like creating a database connection or initializing a complex object. This is where setUp() comes in. Python calls setUp() automatically before every single test method in the class. It's like cleaning your workbench before starting a new task so that a mistake in Test A doesn't bleed into Test B.

class TestShoppingCart(unittest.TestCase):
    def setUp(self):
        # This runs before every test method
        self.cart = ShoppingCart()
        self.cart.add_item("Book", 10.00)

    def test_item_count(self):
        self.assertEqual(len(self.cart.items), 1)

    def test_total_price(self):
        self.assertEqual(self.cart.get_total(), 10.00)

One last tip: don't try to test everything. Focus on the "edge cases"—the weird inputs, the empty lists, the negative numbers. That's where the bugs usually hide. If you only test the "happy path" where everything goes right, you aren't really testing; you're just confirming your code can do what it was designed to do.




📋 Practical Task

Exercise: Validating a Password Strength Checker

You have been handed a PasswordChecker class that is supposed to enforce security rules. However, the original developer didn't write any tests, and you suspect the logic is flawed. Your goal is to write a comprehensive test suite using the unittest module to verify its behavior.

The Requirements for a "Strong" Password:

  • Must be at least 8 characters long.
  • Must contain at least one digit.
  • Must contain at least one uppercase letter.
# The code to test
class PasswordChecker:
    def is_strong(self, password):
        if len(password) < 8:
            return False
        if not any(char.isdigit() for char in password):
            return False
        if not any(char.isupper() for char in password):
            return False
        return True

Your Task:

  1. Create a test class TestPasswordChecker inheriting from unittest.TestCase.
  2. Use setUp()` to initialize an instance of PasswordChecker.
  3. Write a test method that verifies a truly strong password (e.g., "SecurePass123") returns True.
  4. Write three separate test methods to verify that passwords failing each individual requirement (too short, no digits, no uppercase) return False.
  5. Run your tests using unittest.main() to ensure everything passes.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.