SE Notes
Testing individual software components in isolation.
Unit testing is the practice of testing individual components of software—functions, methods, or classes—in complete isolation from the rest of the system. Each unit test verifies that a single, small piece of code behaves correctly for specific inputs. Unit tests are the foundation of the testing pyramid: they are the fastest to execute, cheapest to maintain, and most numerous tests in a well-tested system. A typical production application might have thousands of unit tests that execute in seconds, providing immediate feedback to developers about whether their changes break existing behavior.
What Constitutes a "Unit"
The definition of a "unit" varies by context: in procedural programming, it is typically a single function. In object-oriented programming, it is often a class or a single method. The key characteristic is that the unit is tested in isolation—all external dependencies (databases, file systems, network services, other classes) are replaced with test doubles (mocks, stubs, or fakes) so that the test verifies only the unit's logic, not its interactions with the broader system.
Anatomy of a Unit Test
Every unit test follows the Arrange-Act-Assert (AAA) pattern:
# Arrange: Set up the test conditions
def test_calculate_discount_for_premium_customer():
customer = Customer(tier="premium")
order = Order(customer=customer, total=500.00)
calculator = DiscountCalculator()
# Act: Execute the code under test
discount = calculator.calculate(order)
# Assert: Verify the expected outcome
assert discount == 75.00 # Premium customers get 15% off
def test_calculate_discount_for_regular_customer():
customer = Customer(tier="regular")
order = Order(customer=customer, total=500.00)
calculator = DiscountCalculator()
discount = calculator.calculate(order)
assert discount == 25.00 # Regular customers get 5% off
def test_no_discount_for_small_order():
customer = Customer(tier="premium")
order = Order(customer=customer, total=10.00)
calculator = DiscountCalculator()
discount = calculator.calculate(order)
assert discount == 0 # Orders under $50 get no discountTest Doubles: Mocks, Stubs, and Fakes
When the unit depends on external components, test doubles replace those dependencies:
Stub: Returns predetermined data without any logic. Used when you need the dependency to provide input to the unit under test.
# Stub: Always returns a fixed exchange rate
class StubExchangeRateService:
def get_rate(self, from_currency, to_currency):
return 1.25 # Fixed rate for testingMock: Records interactions and verifies that expected calls were made. Used when you need to verify the unit interacts correctly with its dependencies.
# Mock: Verifies that email service was called correctly
def test_order_sends_confirmation_email():
mock_email = Mock()
order_service = OrderService(email_service=mock_email)
order_service.place_order(order)
mock_email.send.assert_called_once_with(
to="customer@example.com",
subject="Order Confirmed"
)Fake: A simplified but working implementation. Used when a full implementation is too complex for testing but simple logic is needed.
Test-Driven Development (TDD)
TDD inverts the traditional order—write the test before writing the code:
- Red: Write a failing test that defines desired behavior
- Green: Write the minimum code to make the test pass
- Refactor: Improve the code while keeping tests passing
# Step 1 - RED: Write failing test
def test_password_requires_minimum_8_characters():
assert validate_password("short") == False
# Step 2 - GREEN: Minimum code to pass
def validate_password(password):
return len(password) >= 8
# Step 3 - REFACTOR: (no refactoring needed yet, add next test)
def test_password_requires_uppercase():
assert validate_password("alllowercase1!") == FalseReal-World Example: Shopping Cart Unit Tests
class TestShoppingCart:
def test_empty_cart_has_zero_total(self):
cart = ShoppingCart()
assert cart.get_total() == 0.00
def test_add_single_item(self):
cart = ShoppingCart()
cart.add_item(Product("Widget", price=9.99), quantity=1)
assert cart.get_total() == 9.99
def test_add_multiple_items(self):
cart = ShoppingCart()
cart.add_item(Product("Widget", price=9.99), quantity=2)
cart.add_item(Product("Gadget", price=14.99), quantity=1)
assert cart.get_total() == 34.97
def test_remove_item_reduces_total(self):
cart = ShoppingCart()
cart.add_item(Product("Widget", price=9.99), quantity=3)
cart.remove_item("Widget", quantity=1)
assert cart.get_total() == 19.98
def test_cannot_add_negative_quantity(self):
cart = ShoppingCart()
with pytest.raises(ValueError):
cart.add_item(Product("Widget", price=9.99), quantity=-1)
def test_bulk_discount_applied_for_10_plus_items(self):
cart = ShoppingCart()
cart.add_item(Product("Widget", price=10.00), quantity=12)
assert cart.get_total() == 108.00 # 10% bulk discountProperties of Good Unit Tests
Fast: Each test should execute in milliseconds. A suite of 5,000 tests should complete in under 10 seconds.
Isolated: Tests should not depend on each other or on shared state. Running tests in any order should produce the same results.
Repeatable: Tests should produce the same results every time, regardless of environment, time of day, or network availability.
Self-validating: Tests should have a clear pass/fail result—no manual interpretation of output.
Timely: Tests should be written close in time to the code they test (ideally before, per TDD).
Common Unit Testing Frameworks
| Language | Framework | Example |
|---|---|---|
| Python | pytest | assert result == expected |
| Java | JUnit 5 | assertEquals(expected, result) |
| JavaScript | Jest | expect(result).toBe(expected) |
| C# | NUnit/xUnit | Assert.Equal(expected, result) |
| Go | testing | if got != want { t.Errorf(...) } |
What NOT to Unit Test
Trivial code (getters/setters with no logic), third-party libraries (trust their own tests), and configuration (environment-specific values). Focus unit testing effort on code with complex logic, calculations, conditional behavior, and edge cases where bugs are most likely to hide.
Integration with CI/CD
Unit tests run automatically on every commit in the CI pipeline. If any test fails, the build is rejected—preventing defective code from reaching other developers or production. This fast feedback loop is why unit test speed matters: developers should not wait minutes to learn their change broke something.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Unit Testing.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Software Engineering topic.
Search Terms
software-engineering, software engineering, software, engineering, testing, unit, unit testing
Related Software Engineering Topics