I fixed a bug last Tuesday. Deployed it. Broke three other things. Spent the rest of the day fixing those. Then broke something else.

The codebase had no tests. Every change was a gamble. I couldn't verify that my fix didn't break existing behavior because there was no definition of what "correct behavior" even meant.

"I don't have time to write tests" is backwards. You don't have time NOT to write tests. The hours you "save" by skipping them get paid back with interest during debugging.

What's a Unit Test?

A unit test verifies one small piece of your code—usually a function. It follows three steps:

  1. Arrange: Set up your data
  2. Act: Call the function
  3. Assert: Check if the result is what you expected

Your First Test

Say you have a function that formats names:

# utils.py
def format_name(first, last):
    if not first or not last:
        raise ValueError("Name parts cannot be empty")
    return f"{last.upper()}, {first.capitalize()}"

Here's how you test it:

# test_utils.py
import unittest
from utils import format_name

class TestNameFormatting(unittest.TestCase):
    
    def test_standard_name(self):
        result = format_name("john", "doe")
        self.assertEqual(result, "DOE, John")
    
    def test_empty_input_raises_error(self):
        with self.assertRaises(ValueError):
            format_name("", "doe")

if __name__ == '__main__':
    unittest.main()

Run it: python test_utils.py. If it prints "OK", your code works. If you later change format_name and break it, this script tells you immediately.

Why This Saves Time

Without tests: You change the code. Run the app. Manually create a user "John Doe". Check output. Try "John" with empty last name. Check error. Takes 5 minutes each time.

With tests: You change the code. Run python test_utils.py. Takes 0.1 seconds. Instant feedback.

Tests are a safety net. They let you refactor without fear.

Test Behavior, Not Implementation

A common mistake is testing HOW code works rather than WHAT it does.

# ❌ Testing internal details
def test_user_internal(self):
    user = User("Alice")
    self.assertEqual(user._name, "Alice")  # Private variable!

# ✅ Testing public behavior  
def test_user_public(self):
    user = User("Alice")
    self.assertEqual(user.get_display_name(), "Alice")

If you test internals, your tests break every time you refactor—even when the output is still correct. Test the public interface, not the private implementation.

Mocking External Dependencies

Real apps call databases and APIs. You don't want tests actually hitting Stripe every time they run. Use mocks:

from unittest.mock import MagicMock

def process_payment(gateway, amount):
    if amount <= 0:
        return False
    return gateway.charge(amount)

class TestPayment(unittest.TestCase):
    def test_payment_success(self):
        fake_gateway = MagicMock()
        fake_gateway.charge.return_value = True
        
        result = process_payment(fake_gateway, 100)
        
        self.assertTrue(result)
        fake_gateway.charge.assert_called_with(100)

The mock pretends to be the real gateway. You're testing your logic in isolation, not whether Stripe's servers are up.

Where to Start

Don't try to test everything at once. Start with:

One test is infinitely better than zero tests. Start small. Add more as you go.

← Back to Debugging & Code Quality

Back to Home