A comprehensive introduction to unit testing concepts and practices in Python, featuring practical examples with pytest.
- What is Unit Testing?
- Why is Unit Testing Important?
- Repository Structure
- Getting Started
- Basic Testing Examples
- Mock Testing Examples
- Running the Tests
- Key Testing Concepts
Unit testing is a software testing method where individual components (units) of a software application are tested in isolation. A "unit" is typically the smallest testable part of an application - often a single function, method, or class.
- Isolated: Each test runs independently without dependencies on other tests
- Fast: Unit tests should execute quickly (milliseconds)
- Repeatable: Tests produce the same results every time they run
- Automated: Tests can run without manual intervention
- Focused: Each test verifies one specific behavior or functionality
- Catch bugs during development, not in production
- Identify issues before they compound into larger problems
- Reduce debugging time significantly
- Refactor code with confidence knowing tests will catch regressions
- Deploy changes with greater assurance
- Document expected behavior through test cases
- Writing testable code often leads to better software architecture
- Forces you to think about dependencies and interfaces
- Encourages modular, loosely-coupled code
- Tests serve as living documentation of how code should behave
- New developers can understand functionality by reading tests
- Examples of proper usage patterns
- Ensure new changes don't break existing functionality
- Maintain code quality as the project grows
- Enable continuous integration and deployment
- Python 3.9+
- uv
- Clone or download this repository
- Navigate to the project directory
- Install dependencies:
uv syncThe tests/example_code/basic_tests/ folder demonstrates fundamental unit testing
concepts using a simple Calculator class.
def test_adding_two_numbers_works(calc):
assert calc.add(3, 4) == 7
assert calc.add(-1, 1) == 0@pytest.fixture
def calc():
"""Provides a fresh Calculator instance for each test."""
return Calculator()@pytest.mark.parametrize("a,b,result", [
(10, 2, 5),
(9, 3, 3),
])
def test_dividing_two_numbers_works(calc, a, b, result):
assert calc.divide(a, b) == resultdef test_dividing_by_zero_raises_error(calc):
with pytest.raises(ValueError):
calc.divide(1, 0)The tests/example_code/mock_tests/ folder showcases different mocking techniques
for various scenarios.
Demonstrates how to mock function dependencies:
def test_compute_discounted_price_with_mock():
mock_discount = Mock(return_value=0.2)
result = compute_discounted_price(100, mock_discount)
assert result == 80.0
mock_discount.assert_called_once()Shows how to mock object methods and simulate different scenarios:
def test_process_payment_success():
mock_gateway = Mock()
mock_gateway.charge.return_value = None
result = process_payment(100, mock_gateway)
assert result == "success"Illustrates mocking external dependencies like HTTP requests:
@patch("example_code.mock_tests.mock_api_calls.weather.requests.get")
def test_get_weather_with_mocked_requests(mock_get):
mock_response = Mock()
mock_response.json.return_value = {"temp": 25, "desc": "Sunny"}
mock_get.return_value = mock_response
# ... test implementationuv run pytestuv run pytest -v# Run only basic tests
uv run pytest tests/basic_tests/
# Run only mock tests
uv run pytest tests/mock_tests/
# Run specific test file
uv run pytest tests/basic_tests/test_calculator.pyuv run pytest --cov=example_codeuv run pytest --cov=example_code --cov-report term-missinguv run pytest -x- Arrange: Set up test data and conditions
- Act: Execute the code under test
- Assert: Verify the expected outcome
- Use descriptive names that explain what is being tested
- Some possible naming conventions can be found here
- Use specific assertions:
assert result == expected - Test both positive and negative cases
- Verify exceptions are raised when expected
- Mock external dependencies (APIs, databases, file systems)
- Don't mock the code you're testing
- Verify mock interactions when important
- Use
side_effectfor simulating exceptions
- Each test should be able to run in isolation
- Don't rely on test execution order
- Clean up after tests (use fixtures for setup/teardown)
- Aim for high test coverage but focus on critical paths
- Test edge cases and error conditions
- Don't test implementation details, test behavior