Skip to content

Testing & Quality Tools

Comprehensive testing and quality assurance tools ensure your code is reliable, maintainable, and follows best practices.

๐Ÿงช pytest

What it is: Modern Python testing framework

Why important:

  • More readable test syntax than unittest
  • Powerful fixtures for test setup
  • Parametrized testing
  • Rich plugin ecosystem
  • Better error messages

Comparison with unittest

# pytest style - clean and readable
def test_addition():
    assert add(2, 3) == 5

# vs unittest style - verbose
class TestMath(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(add(2, 3), 5)

Key Features

Fixtures

@pytest.fixture
def user_data():
    return {"name": "John", "email": "john@example.com"}

def test_user_creation(user_data):
    user = User(**user_data)
    assert user.name == "John"

Parametrized Tests

@pytest.mark.parametrize("input,expected", [
    (2, 4),
    (3, 9),
    (4, 16),
])
def test_square(input, expected):
    assert square(input) == expected

Configuration

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
    "--strict-markers",
    "--strict-config",
    "--cov=src",
    "--cov-report=term-missing",
    "--cov-report=html",
]

๐Ÿ“Š Coverage

What it is: Measures how much of your code is tested

Why important:

  • Identifies untested code paths
  • Helps maintain high code quality
  • Required by many CI/CD pipelines
  • Shows where more tests are needed

Usage

# Run tests with coverage
pytest --cov=src

# Generate HTML report
pytest --cov=src --cov-report=html

# Set coverage thresholds
pytest --cov=src --cov-fail-under=90

Configuration

[tool.coverage.run]
source = ["src"]
omit = [
    "*/tests/*",
    "*/test_*.py",
    "*/__init__.py",
]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "if self.debug:",
    "if settings.DEBUG",
    "raise AssertionError",
    "raise NotImplementedError",
    "if 0:",
    "if __name__ == .__main__.:",
]
show_missing = true
fail_under = 90

Coverage Reports

  • Terminal: Quick overview with missing lines
  • HTML: Detailed interactive reports
  • XML: For CI/CD integration
  • JSON: For programmatic processing

โšก Tox

What it is: Testing across multiple Python versions

Why important:

  • Ensures compatibility across Python versions
  • Isolated testing environments
  • Standardized testing commands
  • Integration with CI systems

Configuration (tox.ini)

[tox]
envlist = py38,py39,py310,py311,py312,lint,type

[testenv]
deps =
    pytest
    pytest-cov
commands = pytest {posargs}

[testenv:lint]
deps = ruff
commands =
    ruff check src tests
    ruff format --check src tests

[testenv:type]
deps = mypy
commands = mypy src

Usage

# Run all environments
tox

# Run specific environment
tox -e py311

# Run with arguments
tox -e py311 -- -v tests/test_core.py

๐Ÿ”ง Nox

What it is: Flexible task automation (modern Tox alternative)

Why important: - Python-based configuration (vs INI files) - More flexible than Tox - Better integration with modern tools - Easier to customize

Configuration (noxfile.py)

import nox

@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12"])
def tests(session):
    session.install("pytest", "pytest-cov")
    session.install("-e", ".")
    session.run("pytest", *session.posargs)

@nox.session
def lint(session):
    session.install("ruff")
    session.run("ruff", "check", "src", "tests")
    session.run("ruff", "format", "--check", "src", "tests")

@nox.session
def type_check(session):
    session.install("mypy")
    session.install("-e", ".")
    session.run("mypy", "src")

Usage

# List available sessions
nox --list

# Run all sessions
nox

# Run specific session
nox -s tests

# Run with arguments
nox -s tests -- -v tests/test_core.py

๐ŸŽฏ Testing Best Practices

Test Organization

tests/
โ”œโ”€โ”€ conftest.py          # Shared fixtures
โ”œโ”€โ”€ test_core.py         # Core functionality tests
โ”œโ”€โ”€ test_cli.py          # CLI tests
โ”œโ”€โ”€ test_integration.py  # Integration tests
โ””โ”€โ”€ fixtures/            # Test data
    โ”œโ”€โ”€ sample_data.json
    โ””โ”€โ”€ mock_responses.py

Writing Good Tests

1. Clear Test Names

def test_user_creation_with_valid_data():
    """Test that a user can be created with valid data."""
    pass

def test_user_creation_raises_error_with_invalid_email():
    """Test that user creation raises ValueError for invalid email."""
    pass

2. Arrange-Act-Assert Pattern

def test_user_full_name():
    # Arrange
    user = User(first_name="John", last_name="Doe")

    # Act
    full_name = user.get_full_name()

    # Assert
    assert full_name == "John Doe"

3. Use Fixtures for Setup

@pytest.fixture
def sample_user():
    return User(
        first_name="John",
        last_name="Doe",
        email="john@example.com"
    )

def test_user_email_validation(sample_user):
    assert sample_user.is_valid_email()

Testing Different Scenarios

Happy Path

def test_successful_user_login():
    user = User("john@example.com", "password123")
    assert user.login() is True

Error Cases

def test_login_with_wrong_password():
    user = User("john@example.com", "wrong_password")
    with pytest.raises(AuthenticationError):
        user.login()

Edge Cases

def test_empty_username():
    with pytest.raises(ValueError, match="Username cannot be empty"):
        User("", "password123")

Integration with CI/CD

GitHub Actions Example

name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -e .[dev]

    - name: Run tests
      run: pytest --cov=src --cov-report=xml

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3

This comprehensive testing setup ensures your code is thoroughly tested, compatible across Python versions, and maintains high quality standards.