--- name: python-testing description: Write and organize tests for scientific Python packages using pytest. Covers fixtures, parametrization, numerical testing with NumPy utilities, property-based testing with Hypothesis, and CI integration. metadata: assets: - assets/conftest-example.py - assets/github-actions-tests.yml - assets/pyproject-pytest.toml references: - references/common-pitfalls.md - references/scientific-patterns.md - references/test-patterns.md --- # Scientific Python Testing with pytest A comprehensive guide to writing effective tests for scientific Python packages using pytest, following the [Scientific Python Community guidelines](https://learn.scientific-python.org/development/guides/pytest/) and [testing tutorial](https://learn.scientific-python.org/development/tutorials/test/). This skill focuses on modern testing patterns, fixtures, parametrization, and best practices specific to scientific computing. ## Quick Reference Card **Common Testing Tasks - Quick Decisions:** ```python # 1. Basic test → Use simple assert def test_function(): assert result == expected # 2. Floating-point comparison → Use approx from pytest import approx assert result == approx(0.333, rel=1e-6) # 3. Testing exceptions → Use pytest.raises with pytest.raises(ValueError, match="must be positive"): function(-1) # 4. Multiple inputs → Use parametrize @pytest.mark.parametrize("input,expected", [(1,1), (2,4), (3,9)]) def test_square(input, expected): assert input**2 == expected # 5. Reusable setup → Use fixture @pytest.fixture def sample_data(): return np.array([1, 2, 3, 4, 5]) # 6. NumPy arrays → Use approx or numpy.testing assert np.mean(data) == approx(3.0) ``` **Decision Tree:** - Need multiple test cases with same logic? → **Parametrize** - Need reusable test data/setup? → **Fixture** - Testing floating-point results? → **pytest.approx** - Testing exceptions/warnings? → **pytest.raises / pytest.warns** - Complex numerical arrays? → **numpy.testing.assert_allclose** - Organizing by speed? → **Markers and separate directories** ## When to Use This Skill - Writing tests for scientific Python packages and libraries - Testing numerical algorithms and scientific computations - Setting up test infrastructure for research software - Implementing continuous integration for scientific code - Testing data analysis pipelines and workflows - Validating scientific simulations and models - Ensuring reproducibility and correctness of research code - Testing code that uses NumPy, SciPy, Pandas, and other scientific libraries ## Core Concepts ### 1. Why pytest for Scientific Python pytest is the de facto standard for testing Python packages because it: - **Simple syntax**: Just use Python's `assert` statement - **Detailed reporting**: Clear, informative failure messages - **Powerful features**: Fixtures, parametrization, marks, plugins - **Scientific ecosystem**: Native support for NumPy arrays, approximate comparisons - **Community standard**: Used by NumPy, SciPy, Pandas, scikit-learn, and more ### 2. Test Structure and Organization **Standard test directory layout:** ```text my-package/ ├── src/ │ └── my_package/ │ ├── __init__.py │ ├── analysis.py │ └── utils.py ├── tests/ │ ├── conftest.py │ ├── test_analysis.py │ └── test_utils.py └── pyproject.toml ``` **Key principles:** - Tests directory separate from source code (alongside `src/`) - Test files named `test_*.py` (pytest discovery) - Test functions named `test_*` (pytest discovery) - No `__init__.py` in tests directory (avoid importability issues) - Test against installed package, not local source ### 3. pytest Configuration See [assets/pyproject-pytest.toml](assets/pyproject-pytest.toml) for a complete pytest configuration example. Basic configuration in `pyproject.toml`: ```toml [tool.pytest.ini_options] minversion = "7.0" addopts = [ "-ra", # Show summary of all test outcomes "--showlocals", # Show local variables in tracebacks "--strict-markers", # Error on undefined markers "--strict-config", # Error on config issues ] testpaths = ["tests"] ``` ## Testing Principles Following the [Scientific Python testing recommendations](https://learn.scientific-python.org/development/principles/testing/), effective testing provides multiple benefits and should follow key principles: ### Advantages of Testing - **Trustworthy code**: Well-tested code behaves as expected and can be relied upon - **Living documentation**: Tests communicate intent and expected behavior, validated with each run - **Preventing failure**: Tests protect against implementation errors and unexpected dependency changes - **Confidence when making changes**: Thorough test suites enable adding features, fixing bugs, and refactoring with confidence ### Fundamental Principles **1. Any test case is better than none** When in doubt, write the test that makes sense at the time: - Test critical behaviors, features, and logic - Write clear, expressive, well-documented tests - Tests are documentation of developer intentions - Good tests make it clear what they are testing and how Don't get bogged down in taxonomy when learning—focus on writing tests that work. **2. As long as that test is correct** It's surprisingly easy to write tests that pass when they should fail: - **Check that your test fails when it should**: Deliberately break the code and verify the test fails - **Keep it simple**: Excessive mocks and fixtures make it difficult to know what's being tested - **Test one thing at a time**: A single test should test a single behavior **3. Start with Public Interface Tests** Begin by testing from the perspective of a user: - Test code as users will interact with it - Keep tests simple and readable for documentation purposes - Focus on supported use cases - Avoid testing private attributes - Minimize use of mocks/patches **4. Organize Tests into Suites** Divide tests by type and execution time for efficiency: - **Unit tests**: Fast, isolated tests of individual components - **Integration tests**: Tests of component interactions and dependencies - **End-to-end tests**: Complete workflow testing Benefits: - Run relevant tests quickly and frequently - "Fail fast" by running fast suites first - Easier to read and reason about - Avoid false positives from expected external failures ### Outside-In Testing Approach The recommended approach is **outside-in**, starting from the user's perspective: 1. **Public Interface Tests**: Test from user perspective, focusing on behavior and features 2. **Integration Tests**: Test that components work together and with dependencies 3. **Unit Tests**: Test individual units in isolation, optimized for speed This approach ensures you're building the right thing before optimizing implementation details. ## Quick Start ### Minimal Test Example ```python # tests/test_basic.py def test_simple_math(): """Test basic arithmetic.""" assert 4 == 2**2 def test_string_operations(): """Test string methods.""" result = "hello world".upper() assert result == "HELLO WORLD" assert "HELLO" in result ``` ### Scientific Test Example ```python # tests/test_scientific.py import numpy as np from pytest import approx from my_package.analysis import compute_mean, fit_linear def test_compute_mean(): """Test mean calculation.""" data = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) result = compute_mean(data) assert result == approx(3.0) def test_fit_linear(): """Test linear regression.""" x = np.array([0, 1, 2, 3, 4]) y = np.array([0, 2, 4, 6, 8]) slope, intercept = fit_linear(x, y) assert slope == approx(2.0) assert intercept == approx(0.0) ``` ## Testing Patterns See [references/test-patterns.md](references/test-patterns.md) for detailed patterns including: - Writing simple, focused tests - Testing for failures - Approximate comparisons - Using fixtures - Parametrized tests - Test organization with markers - Mocking and monkeypatching - Testing against installed version - Import best practices ## Scientific Python Testing Patterns See [references/scientific-patterns.md](references/scientific-patterns.md) for scientific-specific patterns: - Testing numerical algorithms - Testing with different NumPy dtypes - Testing random/stochastic code - Testing data pipelines - Property-based testing with Hypothesis ## Running pytest ### Basic Usage ```bash # Run all tests pytest # Run specific file pytest tests/test_analysis.py # Run specific test pytest tests/test_analysis.py::test_mean # Run tests matching pattern pytest -k "mean or median" # Verbose output pytest -v # Show local variables in failures pytest -l # or --showlocals # Stop at first failure pytest -x # Show stdout/stderr pytest -s ``` ### Debugging Tests ```bash # Drop into debugger on failure pytest --pdb # Drop into debugger at start of each test pytest --trace # Run last failed tests pytest --lf # Run failed tests first, then rest pytest --ff # Show which tests would be run (dry run) pytest --collect-only ``` ### Coverage ```bash # Install pytest-cov pip install pytest-cov # Run with coverage pytest --cov=my_package # With coverage report pytest --cov=my_package --cov-report=html # With missing lines pytest --cov=my_package --cov-report=term-missing # Fail if coverage below threshold pytest --cov=my_package --cov-fail-under=90 ``` See [assets/pyproject-pytest.toml](assets/pyproject-pytest.toml) for complete coverage configuration. ## File Templates and Examples Ready-to-use templates are available in the `assets/` directory: - **[assets/pyproject-pytest.toml](assets/pyproject-pytest.toml)** - Complete pytest configuration with coverage - **[assets/conftest-example.py](assets/conftest-example.py)** - Example conftest.py with shared fixtures - **[assets/github-actions-tests.yml](assets/github-actions-tests.yml)** - GitHub Actions workflow for testing ## Common Pitfalls and Solutions See [references/common-pitfalls.md](references/common-pitfalls.md) for solutions to: - Testing implementation instead of behavior - Non-deterministic tests - Exact floating-point comparisons - Testing too much in one test ## Testing Checklist - [ ] Tests are in `tests/` directory separate from source - [ ] Test files named `test_*.py` - [ ] Test functions named `test_*` - [ ] Tests run against installed package (use src/ layout) - [ ] pytest configured in `pyproject.toml` - [ ] Using `pytest.approx` for floating-point comparisons - [ ] Tests check exceptions with `pytest.raises` - [ ] Tests check warnings with `pytest.warns` - [ ] Parametrized tests for multiple inputs - [ ] Fixtures for reusable setup - [ ] Markers used for test organization - [ ] Random tests use fixed seeds - [ ] Tests are independent (can run in any order) - [ ] Each test focuses on one behavior - [ ] Coverage > 80% (preferably > 90%) - [ ] All tests pass before committing - [ ] Slow tests marked with `@pytest.mark.slow` - [ ] Integration tests marked appropriately - [ ] CI configured to run tests automatically ## Continuous Integration See [assets/github-actions-tests.yml](assets/github-actions-tests.yml) for a complete GitHub Actions workflow example. ## Resources - **Scientific Python pytest Guide**: - **Scientific Python Testing Tutorial**: - **Scientific Python Testing Principles**: - **pytest Documentation**: - **pytest-cov**: - **pytest-mock**: - **Hypothesis (property-based testing)**: - **NumPy testing utilities**: - **Testing best practices**: ## Summary Testing scientific Python code with pytest, following Scientific Python community principles, provides: 1. **Confidence**: Know your code works correctly 2. **Reproducibility**: Ensure consistent behavior across environments 3. **Documentation**: Tests show how code should be used and communicate developer intent 4. **Refactoring safety**: Change code without breaking functionality 5. **Regression prevention**: Catch bugs before they reach users 6. **Scientific rigor**: Validate numerical accuracy and physical correctness **Key testing principles:** - Start with **public interface tests** from the user's perspective - Organize tests into **suites** (unit, integration, e2e) by type and speed - Follow **outside-in** approach: public interface → integration → unit tests - Keep tests **simple, focused, and independent** - Test **behavior rather than implementation** - Use pytest's powerful features (fixtures, parametrization, markers) effectively - Always verify tests **fail when they should** to avoid false confidence **Remember**: Any test is better than none, but well-organized tests following these principles create trustworthy, maintainable scientific software that the community can rely on.