Unit Testing Basics: Why Your Code Needs a Safety Net
Unit Testing: Your Code’s Insurance Policy
As a newbie, you probably don’t understand this yet, but…
You’re going to write code that works perfectly… until it doesn’t.
Maybe you change one little thing and suddenly your entire program explodes. Maybe you fix a bug and accidentally create three new ones. Maybe you’re working on a team and someone else’s “harmless” change breaks your code.
Welcome to programming reality.
Unit testing is your insurance policy against this chaos. It’s like having a robot that constantly checks if your code still works the way you intended.
What the Heck is Unit Testing?
Think of unit testing like quality control in a factory. Before a car leaves the assembly line, someone checks that the brakes work, the lights turn on, and the engine starts. Unit tests do the same thing for your code - they check that each piece (or “unit”) of your program works correctly.
A “unit” is usually a single method or function. You write tests that:
- Call your method with specific inputs
- Check that it returns the expected output
- Verify it behaves correctly in edge cases
Why You Actually Need This (Even Though It Feels Like Extra Work)
Let’s be honest - when you’re learning to code, writing tests feels like doing homework twice. You already wrote the code, why write more code to test it?
Here’s why you’ll thank yourself later:
Catch Bugs Before They Embarrass You
Nothing’s worse than demoing your app and having it crash because you forgot to handle empty input. Tests catch this stuff.
Refactor Without Fear
Want to optimize that messy function? With tests, you can rewrite it completely and know immediately if you broke anything.
Sleep Better at Night
When your test suite passes, you know your code works. When you push to production, you’re confident instead of terrified.
Look Professional
Real developers write tests. Period. It’s the difference between “person who codes” and “software engineer.”
The Testing Mindset
Before we dive into code, understand this: good tests are mean to your code.
They try to break it. They pass weird inputs. They test edge cases you never thought of. A good test is basically asking your code: “Yeah, but what if…?”
- What if someone passes
null
? - What if the input is negative when it should be positive?
- What if the list is empty?
- What if the string has weird characters?
Your job is to write code that handles these scenarios gracefully, and tests that verify it does.
Java Unit Testing with JUnit4
Java uses JUnit for testing. It’s been around forever and it works great. Here are three examples that show the progression from simple to realistic.
Example 1: Testing a Simple Calculator Class
1// Calculator.java - The class we want to test
2public class Calculator {
3 public int add(int a, int b) {
4 return a + b;
5 }
6
7 public double divide(double a, double b) {
8 if (b == 0) {
9 throw new IllegalArgumentException("Cannot divide by zero");
10 }
11 return a / b;
12 }
13
14 public boolean isEven(int number) {
15 return number % 2 == 0;
16 }
17}
18
19// CalculatorTest.java - Our tests
20import org.junit.Test;
21import org.junit.Before;
22import static org.junit.Assert.*;
23
24public class CalculatorTest {
25 private Calculator calculator;
26
27 @Before
28 public void setUp() {
29 // This runs before each test method
30 calculator = new Calculator();
31 }
32
33 @Test
34 public void testAdd() {
35 // Test normal addition
36 int result = calculator.add(2, 3);
37 assertEquals(5, result);
38
39 // Test adding negative numbers
40 result = calculator.add(-1, -1);
41 assertEquals(-2, result);
42
43 // Test adding zero
44 result = calculator.add(5, 0);
45 assertEquals(5, result);
46 }
47
48 @Test
49 public void testDivide() {
50 // Test normal division
51 double result = calculator.divide(10.0, 2.0);
52 assertEquals(5.0, result, 0.001); // The 0.001 is for floating point precision
53
54 // Test division with decimals
55 result = calculator.divide(7.0, 3.0);
56 assertEquals(2.333, result, 0.001);
57 }
58
59 @Test(expected = IllegalArgumentException.class)
60 public void testDivideByZero() {
61 // This test expects an exception to be thrown
62 calculator.divide(5.0, 0.0);
63 }
64
65 @Test
66 public void testIsEven() {
67 // Test even numbers
68 assertTrue(calculator.isEven(4));
69 assertTrue(calculator.isEven(0));
70 assertTrue(calculator.isEven(-2));
71
72 // Test odd numbers
73 assertFalse(calculator.isEven(3));
74 assertFalse(calculator.isEven(-1));
75 }
76}
What to Notice:
@Before
sets up a fresh calculator for each test (no contamination between tests)- Each
@Test
method focuses on one specific behavior - We test normal cases AND edge cases (negative numbers, zero, etc.)
assertEquals
checks if two values are equalassertTrue
/assertFalse
check boolean conditions- We explicitly test that exceptions are thrown when they should be
Example 2: Testing a Bank Account Class (More Realistic)
1// BankAccount.java
2public class BankAccount {
3 private double balance;
4 private boolean isActive;
5
6 public BankAccount(double initialBalance) {
7 if (initialBalance < 0) {
8 throw new IllegalArgumentException("Initial balance cannot be negative");
9 }
10 this.balance = initialBalance;
11 this.isActive = true;
12 }
13
14 public void deposit(double amount) {
15 if (!isActive) {
16 throw new IllegalStateException("Account is closed");
17 }
18 if (amount <= 0) {
19 throw new IllegalArgumentException("Deposit amount must be positive");
20 }
21 balance += amount;
22 }
23
24 public void withdraw(double amount) {
25 if (!isActive) {
26 throw new IllegalStateException("Account is closed");
27 }
28 if (amount <= 0) {
29 throw new IllegalArgumentException("Withdrawal amount must be positive");
30 }
31 if (amount > balance) {
32 throw new IllegalArgumentException("Insufficient funds");
33 }
34 balance -= amount;
35 }
36
37 public double getBalance() {
38 return balance;
39 }
40
41 public void closeAccount() {
42 isActive = false;
43 }
44
45 public boolean isActive() {
46 return isActive;
47 }
48}
49
50// BankAccountTest.java
51import org.junit.Test;
52import org.junit.Before;
53import static org.junit.Assert.*;
54
55public class BankAccountTest {
56 private BankAccount account;
57
58 @Before
59 public void setUp() {
60 account = new BankAccount(100.0); // Start each test with $100
61 }
62
63 @Test
64 public void testAccountCreation() {
65 BankAccount newAccount = new BankAccount(50.0);
66 assertEquals(50.0, newAccount.getBalance(), 0.01);
67 assertTrue(newAccount.isActive());
68 }
69
70 @Test(expected = IllegalArgumentException.class)
71 public void testNegativeInitialBalance() {
72 new BankAccount(-10.0); // Should throw exception
73 }
74
75 @Test
76 public void testDeposit() {
77 account.deposit(25.0);
78 assertEquals(125.0, account.getBalance(), 0.01);
79
80 // Test multiple deposits
81 account.deposit(10.0);
82 account.deposit(5.0);
83 assertEquals(140.0, account.getBalance(), 0.01);
84 }
85
86 @Test(expected = IllegalArgumentException.class)
87 public void testDepositNegativeAmount() {
88 account.deposit(-5.0);
89 }
90
91 @Test(expected = IllegalArgumentException.class)
92 public void testDepositZeroAmount() {
93 account.deposit(0.0);
94 }
95
96 @Test
97 public void testWithdraw() {
98 account.withdraw(30.0);
99 assertEquals(70.0, account.getBalance(), 0.01);
100 }
101
102 @Test(expected = IllegalArgumentException.class)
103 public void testWithdrawMoreThanBalance() {
104 account.withdraw(150.0); // More than the $100 balance
105 }
106
107 @Test(expected = IllegalArgumentException.class)
108 public void testWithdrawNegativeAmount() {
109 account.withdraw(-10.0);
110 }
111
112 @Test
113 public void testCloseAccount() {
114 account.closeAccount();
115 assertFalse(account.isActive());
116 }
117
118 @Test(expected = IllegalStateException.class)
119 public void testDepositOnClosedAccount() {
120 account.closeAccount();
121 account.deposit(10.0); // Should fail
122 }
123
124 @Test(expected = IllegalStateException.class)
125 public void testWithdrawFromClosedAccount() {
126 account.closeAccount();
127 account.withdraw(10.0); // Should fail
128 }
129}
What to Notice:
- We test the constructor with both valid and invalid inputs
- Each business rule gets its own test (negative deposits, insufficient funds, etc.)
- We test state changes (account closure affects other operations)
- Tests have descriptive names that explain what they’re testing
Example 3: Testing a Shopping Cart (Complex Object Interactions)
1// ShoppingCart.java
2import java.util.*;
3
4public class ShoppingCart {
5 private List<String> items;
6 private Map<String, Double> prices;
7 private double taxRate;
8
9 public ShoppingCart(double taxRate) {
10 this.items = new ArrayList<>();
11 this.prices = new HashMap<>();
12 this.taxRate = taxRate;
13 }
14
15 public void addItem(String item, double price) {
16 if (item == null || item.trim().isEmpty()) {
17 throw new IllegalArgumentException("Item name cannot be empty");
18 }
19 if (price < 0) {
20 throw new IllegalArgumentException("Price cannot be negative");
21 }
22 items.add(item);
23 prices.put(item, price);
24 }
25
26 public void removeItem(String item) {
27 if (!items.contains(item)) {
28 throw new IllegalArgumentException("Item not in cart");
29 }
30 items.remove(item);
31 prices.remove(item);
32 }
33
34 public double getSubtotal() {
35 return prices.values().stream().mapToDouble(Double::doubleValue).sum();
36 }
37
38 public double getTax() {
39 return getSubtotal() * taxRate;
40 }
41
42 public double getTotal() {
43 return getSubtotal() + getTax();
44 }
45
46 public int getItemCount() {
47 return items.size();
48 }
49
50 public boolean isEmpty() {
51 return items.isEmpty();
52 }
53}
54
55// ShoppingCartTest.java
56import org.junit.Test;
57import org.junit.Before;
58import static org.junit.Assert.*;
59
60public class ShoppingCartTest {
61 private ShoppingCart cart;
62 private static final double TAX_RATE = 0.08; // 8% tax
63
64 @Before
65 public void setUp() {
66 cart = new ShoppingCart(TAX_RATE);
67 }
68
69 @Test
70 public void testEmptyCart() {
71 assertTrue(cart.isEmpty());
72 assertEquals(0, cart.getItemCount());
73 assertEquals(0.0, cart.getSubtotal(), 0.01);
74 assertEquals(0.0, cart.getTotal(), 0.01);
75 }
76
77 @Test
78 public void testAddSingleItem() {
79 cart.addItem("Apple", 1.50);
80
81 assertFalse(cart.isEmpty());
82 assertEquals(1, cart.getItemCount());
83 assertEquals(1.50, cart.getSubtotal(), 0.01);
84 assertEquals(0.12, cart.getTax(), 0.01); // 1.50 * 0.08
85 assertEquals(1.62, cart.getTotal(), 0.01); // 1.50 + 0.12
86 }
87
88 @Test
89 public void testAddMultipleItems() {
90 cart.addItem("Apple", 1.50);
91 cart.addItem("Banana", 0.75);
92 cart.addItem("Orange", 2.00);
93
94 assertEquals(3, cart.getItemCount());
95 assertEquals(4.25, cart.getSubtotal(), 0.01); // 1.50 + 0.75 + 2.00
96 assertEquals(0.34, cart.getTax(), 0.01); // 4.25 * 0.08
97 assertEquals(4.59, cart.getTotal(), 0.01); // 4.25 + 0.34
98 }
99
100 @Test(expected = IllegalArgumentException.class)
101 public void testAddItemWithEmptyName() {
102 cart.addItem("", 1.00);
103 }
104
105 @Test(expected = IllegalArgumentException.class)
106 public void testAddItemWithNullName() {
107 cart.addItem(null, 1.00);
108 }
109
110 @Test(expected = IllegalArgumentException.class)
111 public void testAddItemWithNegativePrice() {
112 cart.addItem("Apple", -1.00);
113 }
114
115 @Test
116 public void testRemoveItem() {
117 cart.addItem("Apple", 1.50);
118 cart.addItem("Banana", 0.75);
119
120 cart.removeItem("Apple");
121
122 assertEquals(1, cart.getItemCount());
123 assertEquals(0.75, cart.getSubtotal(), 0.01);
124 }
125
126 @Test(expected = IllegalArgumentException.class)
127 public void testRemoveNonExistentItem() {
128 cart.addItem("Apple", 1.50);
129 cart.removeItem("Orange"); // Orange was never added
130 }
131
132 @Test
133 public void testZeroTaxRate() {
134 ShoppingCart noTaxCart = new ShoppingCart(0.0);
135 noTaxCart.addItem("Apple", 1.50);
136
137 assertEquals(1.50, noTaxCart.getSubtotal(), 0.01);
138 assertEquals(0.0, noTaxCart.getTax(), 0.01);
139 assertEquals(1.50, noTaxCart.getTotal(), 0.01);
140 }
141}
What to Notice:
- We test complex calculations (subtotal, tax, total)
- We verify state consistency (item count matches operations)
- We test edge cases (empty cart, zero tax rate)
- Multiple related operations are tested together
Python Unit Testing with unittest
Python’s built-in unittest
module works similarly to JUnit. Here are the same three examples in Python.
Example 1: Testing a Simple Calculator Class
1# calculator.py - The class we want to test
2class Calculator:
3 def add(self, a, b):
4 return a + b
5
6 def divide(self, a, b):
7 if b == 0:
8 raise ValueError("Cannot divide by zero")
9 return a / b
10
11 def is_even(self, number):
12 return number % 2 == 0
13
14# test_calculator.py - Our tests
15import unittest
16from calculator import Calculator
17
18class TestCalculator(unittest.TestCase):
19 def setUp(self):
20 # This runs before each test method
21 self.calculator = Calculator()
22
23 def test_add(self):
24 # Test normal addition
25 result = self.calculator.add(2, 3)
26 self.assertEqual(result, 5)
27
28 # Test adding negative numbers
29 result = self.calculator.add(-1, -1)
30 self.assertEqual(result, -2)
31
32 # Test adding zero
33 result = self.calculator.add(5, 0)
34 self.assertEqual(result, 5)
35
36 def test_divide(self):
37 # Test normal division
38 result = self.calculator.divide(10.0, 2.0)
39 self.assertAlmostEqual(result, 5.0, places=3)
40
41 # Test division with decimals
42 result = self.calculator.divide(7.0, 3.0)
43 self.assertAlmostEqual(result, 2.333, places=3)
44
45 def test_divide_by_zero(self):
46 # Test that exception is raised
47 with self.assertRaises(ValueError):
48 self.calculator.divide(5.0, 0.0)
49
50 def test_is_even(self):
51 # Test even numbers
52 self.assertTrue(self.calculator.is_even(4))
53 self.assertTrue(self.calculator.is_even(0))
54 self.assertTrue(self.calculator.is_even(-2))
55
56 # Test odd numbers
57 self.assertFalse(self.calculator.is_even(3))
58 self.assertFalse(self.calculator.is_even(-1))
59
60# Run the tests
61if __name__ == '__main__':
62 unittest.main()
What to Notice:
- Python uses
self.assertEqual
instead ofassertEquals
self.assertAlmostEqual
handles floating point precision- Exception testing uses
with self.assertRaises(ValueError):
- Test class inherits from
unittest.TestCase
Example 2: Testing a Bank Account Class
1# bank_account.py
2class BankAccount:
3 def __init__(self, initial_balance):
4 if initial_balance < 0:
5 raise ValueError("Initial balance cannot be negative")
6 self.balance = initial_balance
7 self.is_active = True
8
9 def deposit(self, amount):
10 if not self.is_active:
11 raise RuntimeError("Account is closed")
12 if amount <= 0:
13 raise ValueError("Deposit amount must be positive")
14 self.balance += amount
15
16 def withdraw(self, amount):
17 if not self.is_active:
18 raise RuntimeError("Account is closed")
19 if amount <= 0:
20 raise ValueError("Withdrawal amount must be positive")
21 if amount > self.balance:
22 raise ValueError("Insufficient funds")
23 self.balance -= amount
24
25 def get_balance(self):
26 return self.balance
27
28 def close_account(self):
29 self.is_active = False
30
31 def is_account_active(self):
32 return self.is_active
33
34# test_bank_account.py
35import unittest
36from bank_account import BankAccount
37
38class TestBankAccount(unittest.TestCase):
39 def setUp(self):
40 self.account = BankAccount(100.0) # Start each test with $100
41
42 def test_account_creation(self):
43 new_account = BankAccount(50.0)
44 self.assertAlmostEqual(new_account.get_balance(), 50.0, places=2)
45 self.assertTrue(new_account.is_account_active())
46
47 def test_negative_initial_balance(self):
48 with self.assertRaises(ValueError):
49 BankAccount(-10.0)
50
51 def test_deposit(self):
52 self.account.deposit(25.0)
53 self.assertAlmostEqual(self.account.get_balance(), 125.0, places=2)
54
55 # Test multiple deposits
56 self.account.deposit(10.0)
57 self.account.deposit(5.0)
58 self.assertAlmostEqual(self.account.get_balance(), 140.0, places=2)
59
60 def test_deposit_negative_amount(self):
61 with self.assertRaises(ValueError):
62 self.account.deposit(-5.0)
63
64 def test_deposit_zero_amount(self):
65 with self.assertRaises(ValueError):
66 self.account.deposit(0.0)
67
68 def test_withdraw(self):
69 self.account.withdraw(30.0)
70 self.assertAlmostEqual(self.account.get_balance(), 70.0, places=2)
71
72 def test_withdraw_more_than_balance(self):
73 with self.assertRaises(ValueError):
74 self.account.withdraw(150.0) # More than the $100 balance
75
76 def test_withdraw_negative_amount(self):
77 with self.assertRaises(ValueError):
78 self.account.withdraw(-10.0)
79
80 def test_close_account(self):
81 self.account.close_account()
82 self.assertFalse(self.account.is_account_active())
83
84 def test_deposit_on_closed_account(self):
85 self.account.close_account()
86 with self.assertRaises(RuntimeError):
87 self.account.deposit(10.0)
88
89 def test_withdraw_from_closed_account(self):
90 self.account.close_account()
91 with self.assertRaises(RuntimeError):
92 self.account.withdraw(10.0)
93
94if __name__ == '__main__':
95 unittest.main()
Example 3: Testing a Shopping Cart
1# shopping_cart.py
2class ShoppingCart:
3 def __init__(self, tax_rate):
4 self.items = []
5 self.prices = {}
6 self.tax_rate = tax_rate
7
8 def add_item(self, item, price):
9 if not item or not item.strip():
10 raise ValueError("Item name cannot be empty")
11 if price < 0:
12 raise ValueError("Price cannot be negative")
13 self.items.append(item)
14 self.prices[item] = price
15
16 def remove_item(self, item):
17 if item not in self.items:
18 raise ValueError("Item not in cart")
19 self.items.remove(item)
20 del self.prices[item]
21
22 def get_subtotal(self):
23 return sum(self.prices.values())
24
25 def get_tax(self):
26 return self.get_subtotal() * self.tax_rate
27
28 def get_total(self):
29 return self.get_subtotal() + self.get_tax()
30
31 def get_item_count(self):
32 return len(self.items)
33
34 def is_empty(self):
35 return len(self.items) == 0
36
37# test_shopping_cart.py
38import unittest
39from shopping_cart import ShoppingCart
40
41class TestShoppingCart(unittest.TestCase):
42 TAX_RATE = 0.08 # 8% tax
43
44 def setUp(self):
45 self.cart = ShoppingCart(self.TAX_RATE)
46
47 def test_empty_cart(self):
48 self.assertTrue(self.cart.is_empty())
49 self.assertEqual(self.cart.get_item_count(), 0)
50 self.assertAlmostEqual(self.cart.get_subtotal(), 0.0, places=2)
51 self.assertAlmostEqual(self.cart.get_total(), 0.0, places=2)
52
53 def test_add_single_item(self):
54 self.cart.add_item("Apple", 1.50)
55
56 self.assertFalse(self.cart.is_empty())
57 self.assertEqual(self.cart.get_item_count(), 1)
58 self.assertAlmostEqual(self.cart.get_subtotal(), 1.50, places=2)
59 self.assertAlmostEqual(self.cart.get_tax(), 0.12, places=2) # 1.50 * 0.08
60 self.assertAlmostEqual(self.cart.get_total(), 1.62, places=2) # 1.50 + 0.12
61
62 def test_add_multiple_items(self):
63 self.cart.add_item("Apple", 1.50)
64 self.cart.add_item("Banana", 0.75)
65 self.cart.add_item("Orange", 2.00)
66
67 self.assertEqual(self.cart.get_item_count(), 3)
68 self.assertAlmostEqual(self.cart.get_subtotal(), 4.25, places=2) # 1.50 + 0.75 + 2.00
69 self.assertAlmostEqual(self.cart.get_tax(), 0.34, places=2) # 4.25 * 0.08
70 self.assertAlmostEqual(self.cart.get_total(), 4.59, places=2) # 4.25 + 0.34
71
72 def test_add_item_with_empty_name(self):
73 with self.assertRaises(ValueError):
74 self.cart.add_item("", 1.00)
75
76 def test_add_item_with_none_name(self):
77 with self.assertRaises(ValueError):
78 self.cart.add_item(None, 1.00)
79
80 def test_add_item_with_negative_price(self):
81 with self.assertRaises(ValueError):
82 self.cart.add_item("Apple", -1.00)
83
84 def test_remove_item(self):
85 self.cart.add_item("Apple", 1.50)
86 self.cart.add_item("Banana", 0.75)
87
88 self.cart.remove_item("Apple")
89
90 self.assertEqual(self.cart.get_item_count(), 1)
91 self.assertAlmostEqual(self.cart.get_subtotal(), 0.75, places=2)
92
93 def test_remove_nonexistent_item(self):
94 self.cart.add_item("Apple", 1.50)
95 with self.assertRaises(ValueError):
96 self.cart.remove_item("Orange") # Orange was never added
97
98 def test_zero_tax_rate(self):
99 no_tax_cart = ShoppingCart(0.0)
100 no_tax_cart.add_item("Apple", 1.50)
101
102 self.assertAlmostEqual(no_tax_cart.get_subtotal(), 1.50, places=2)
103 self.assertAlmostEqual(no_tax_cart.get_tax(), 0.0, places=2)
104 self.assertAlmostEqual(no_tax_cart.get_total(), 1.50, places=2)
105
106if __name__ == '__main__':
107 unittest.main()
Running Your Tests
Java (JUnit4)
1# Compile your classes and tests
2javac -cp .:junit-4.12.jar:hamcrest-core-1.3.jar *.java
3
4# Run the tests
5java -cp .:junit-4.12.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore CalculatorTest
Python (unittest)
1# Run a specific test file
2python test_calculator.py
3
4# Run all tests in the current directory
5python -m unittest discover
6
7# Run with more verbose output
8python -m unittest -v test_calculator.py
The Reality Check
When you’re starting out, writing tests feels like:
- Extra work - “I already wrote the code, why write more code?”
- Slowing you down - “I could build three features in the time it takes to test one”
- Pointless - “My code obviously works, I just tested it manually”
But here’s what happens as you grow as a developer:
Week 1: “Tests are stupid, they slow me down”
Month 3: “Okay, tests caught a few bugs, but I still don’t have time”
Year 1: “Holy crap, my tests just saved me from deploying broken code”
Year 2: “I refuse to write code without tests”
The transition happens when you experience your first major bug that tests would have caught, or when you need to refactor a complex piece of code and your tests give you the confidence to do it fearlessly.
Your Testing Journey
Start small:
- Write tests for new methods - Don’t retrofit your entire codebase
- Test the scary parts - Complex logic, edge cases, anything involving math
- Test when you find bugs - Write a test that reproduces the bug, then fix it
- Make it a habit - Eventually, you’ll feel uncomfortable shipping untested code
Remember: Good code is tested code. And tested code is code you can trust.
testAdd()
is okay, but testAddWithNegativeNumbers()
tells you exactly what broke when the test fails at 2 AM.