Unit Testing Basics in Python (unittest & pytest)

Module 4 • Session 32 • Allocation: 1 Contact Hr

unittest pytest TestCase assert mocking

Unit testing validates individual components of code independently to ensure they meet requirements and produce expected outputs. Python offers the built-in unittest standard library framework along with the third-party pytest runner for clean, automated test execution.

1. Standard Library Testing (unittest Module)

The unittest module follows object-oriented concepts modeled after xUnit frameworks. Test cases inherit from unittest.TestCase and define methods starting with the prefix test_.

Method / Hook Category Purpose / Behavior
assertEqual(a, b) Assertion Verifies that a == b.
assertTrue(expr) Assertion Verifies that bool(expr) evaluates to True.
assertRaises(Exc, func) Assertion Verifies that calling func raises the expected exception type.
setUp() Lifecycle Hook Executes automatically before every single test method in the class.
tearDown() Lifecycle Hook Executes automatically after every single test method completes.

Example 1: Building a Complete TestCase with unittest

Here is a complete test file structure verifying custom arithmetic and exception validation routines:

import unittest # Logic to be tested def divide(a: float, b: float) -> float: if b == 0: raise ValueError("Cannot divide by zero.") return a / b class TestCalculatorOperations(unittest.TestCase): def setUp(self): # Fixture initialization before each test self.numerator = 100.0 def test_valid_division(self): result = divide(self.numerator, 4.0) self.assertEqual(result, 25.0) def test_float_precision(self): result = divide(10.0, 3.0) self.assertAlmostEqual(result, 3.3333, places=4) def test_divide_by_zero_exception(self): with self.assertRaises(ValueError): divide(self.numerator, 0) if __name__ == '__main__': unittest.main()
Key Rule: Every test method name inside a TestCase class MUST begin with test_. Methods without this prefix will be ignored by test runners.

2. Modern Testing with pytest

pytest simplifies test suites by allowing plain Python assert statements, functional test declarations, and flexible fixture injection mechanisms.

Comparison: unittest vs pytest

Feature unittest pytest
Setup Boilerplate Requires subclassing unittest.TestCase Simple plain functions or classes
Assertions Requires methods (self.assertEqual) Native Python assert statements
Fixtures Class hooks (setUp/tearDown) Modular @pytest.fixture decorators
Parameterization Requires custom loops or subtests Native @pytest.mark.parametrize

Example 2: Concise Test Writing with pytest & Fixtures

The code below shows how pytest uses fixtures and parameterization for clean test design:

import pytest # Application target class class BankAccount: def __init__(self, balance: float = 0.0): self.balance = balance def deposit(self, amount: float): if amount <= 0: raise ValueError("Deposit amount must be positive.") self.balance += amount # Pytest fixture injecting clean initial state @pytest.fixture def account(): return BankAccount(balance=100.0) # 1. Using fixture parameter def test_initial_balance(account): assert account.balance == 100.0 def test_deposit_valid_amount(account): account.deposit(50.0) assert account.balance == 150.0 # 2. Testing raised exceptions def test_invalid_deposit(account): with pytest.raises(ValueError): account.deposit(-20.0) # 3. Parameterized Test Case @pytest.mark.parametrize("initial, deposit_amt, expected", [ (0.0, 100.0, 100.0), (50.0, 25.50, 75.50), (1000.0, 500.0, 1500.0) ]) def test_multiple_deposits(initial, deposit_amt, expected): acc = BankAccount(initial) acc.deposit(deposit_amt) assert acc.balance == expected

3. Isolating Dependencies with Mocking (unittest.mock)

When unit tests rely on external services (such as databases or third-party web APIs), use unittest.mock.Mock or patch to simulate dependencies and isolate your target logic.

import unittest from unittest.mock import Mock, patch def fetch_user_status(api_client, user_id): # Function fetching data via external API client dependency response = api_client.get(f"/users/{user_id}") if response.get("status_code") == 200: return response.get("data", {}).get("status") return "UNKNOWN" class TestMockingApiCall(unittest.TestCase): def test_fetch_user_status_success(self): # 1. Create a mock API client instance mock_client = Mock() # 2. Define expected return value for the .get() method call mock_client.get.return_value = { "status_code": 200, "data": {"status": "ACTIVE"} } # 3. Execute unit test against mocked dependency status = fetch_user_status(mock_client, user_id=42) # 4. Assert behavior and verify client invocation details self.assertEqual(status, "ACTIVE") mock_client.get.assert_called_once_with("/users/42") if __name__ == '__main__': unittest.main()
Best Practice: Keep unit tests isolated, deterministic, and fast. External file or network operations should always be mocked out in unit test suites.

🏋️ Try It Yourself: Practice Challenges

Challenge 1: String Formatter Test Suite

Write a unittest.TestCase that verifies a function format_name(first, last) returns "Last, First", strips excess whitespace, and handles missing last names correctly.

Challenge 2: Exception Validation for Password Checker

Build a function validate_password(pwd) that raises a ValueError if the password is under 8 characters. Write tests ensuring both valid passwords pass and invalid ones raise the expected exception.

Challenge 3: Mocking External Service Calls

Create a class NotificationManager that calls an external email gateway function. Use unittest.mock.Mock to verify that the email sender function is called exactly once with the expected recipient address.

⚡ Interactive Sandbox (unittest Execution Engine)
Console Output:
Click "Run Code" above to execute the unittest suite...

📝 Knowledge Check Quiz

1. What naming convention must test methods follow in unittest.TestCase classes?
2. How are setup operations executed before each test method in pytest versus unittest?
3. What is the primary purpose of mocking external dependencies in unit testing?
Advertisement
Horizontal Ad Banner Slot (728x90 / Responsive)