Debugging with Confidence - An Introduction to Unit Testing
Debugging with Confidence: An Introduction to Unit Testing
One of the keys to writing reliable software is making sure your code works as expected—and keeps working, even as you add new features or make changes. This is where unit testing comes in. Unit tests let you check that small, individual pieces of your code (like functions) are working correctly.
In this post, we’ll:
- Introduce unit testing and why it’s important.
- Add functionality to our expense tracker.
- Write a unit test that exposes a bug.
- Fix the bug and see the test pass.
What is Unit Testing?
A unit test is a small program that tests a single part (or “unit”) of your code. It ensures that specific functions behave as expected, even as your program grows and changes.
Why Unit Testing Matters
- Catches Bugs Early: Find issues before they break your program.
- Supports Refactoring: Confidently change your code knowing tests will catch regressions.
- Improves Code Quality: Forces you to think about edge cases and expected behavior.
Example: Testing Our Expense Tracker
We’ll build on our Simple Expense Tracker from the previous post. Let’s add a new function to find the most expensive item—and expose a bug with a test.
Step 1: Add New Functionality
Let’s add a function called find_most_expensive
to identify the most expensive expense in the list.
Python Implementation:
# Function to find the most expensive expense
def find_most_expensive():
return max(expenses, key=lambda x: x["amount"])
JavaScript Implementation:
// Function to find the most expensive expense
function findMostExpensive() {
return expenses.reduce(
(max, expense) => (expense.amount > max.amount ? expense : max),
expenses[0]
);
}
Step 2: Write Unit Tests
Python Tests (Using unittest
):
import unittest
class TestExpenseTracker(unittest.TestCase):
def setUp(self):
global expenses
expenses = [
{"description": "Lunch", "amount": 12.50},
{"description": "Coffee", "amount": 3.75},
{"description": "Groceries", "amount": 25.00}
]
def test_find_most_expensive(self):
result = find_most_expensive()
self.assertEqual(result["description"], "Groceries")
self.assertEqual(result["amount"], 25.00)
if __name__ == "__main__":
unittest.main()
JavaScript Tests (Using Jest):
const { findMostExpensive } = require("./expenseTracker");
describe("Expense Tracker Tests", () => {
let expenses;
beforeEach(() => {
expenses = [
{ description: "Lunch", amount: 12.5 },
{ description: "Coffee", amount: 3.75 },
{ description: "Groceries", amount: 25.0 },
];
});
test("findMostExpensive should return the most expensive item", () => {
const result = findMostExpensive(expenses);
expect(result.description).toBe("Groceries");
expect(result.amount).toBe(25.0);
});
});
Step 3: Expose a Bug
Let’s introduce a subtle bug: if the expenses
list is empty, the find_most_expensive
function will crash. Run the tests, and you’ll see them fail with an error.
Step 4: Fix the Bug
We’ll update the function to handle an empty list gracefully.
Python Fix:
def find_most_expensive():
if not expenses:
return None
return max(expenses, key=lambda x: x["amount"])
JavaScript Fix:
function findMostExpensive(expenses) {
if (expenses.length === 0) return null;
return expenses.reduce(
(max, expense) => (expense.amount > max.amount ? expense : max),
expenses[0]
);
}
Now re-run the tests—they should pass!
Wrapping Up
Unit tests are an essential tool for writing reliable, maintainable code. They give you confidence that your code works as intended and catch bugs early, saving you time and headaches down the road.
Try writing unit tests for your own projects and see how they transform your coding workflow. Let me know how your tests turn out—and stay tuned for more enhancements to our expense tracker!