Skip to content

docs/contributing/testing.md

Testing Guide

Running Tests

  1. Run all tests:

    uv run pytest
    

  2. Run with coverage:

    uv run pytest --cov=fmp_data
    

  3. Run specific test file:

    uv run pytest tests/unit/test_client.py
    

  4. Run integration tests with VCR replay (default, fast):

    FMP_VCR_RECORD=none uv run pytest tests/integration/
    

  5. Record or refresh VCR cassettes (live API, slower):

    FMP_TEST_API_KEY=your_test_api_key FMP_VCR_RECORD=new_episodes uv run pytest tests/integration/  # pragma: allowlist secret
    

  6. Record only the tests you're touching:

    FMP_TEST_API_KEY=your_test_api_key FMP_VCR_RECORD=new_episodes uv run pytest tests/integration/test_sec.py  # pragma: allowlist secret
    

  7. Full client-method e2e sweep (local, spends quota on record):

    # list every public sync method
    uv run python scripts/e2e_endpoints.py list
    
    # record one group, or everything
    uv run python scripts/e2e_endpoints.py record --group company
    uv run python scripts/e2e_endpoints.py record --skip-bulk
    
    # replay without hitting the API
    uv run python scripts/e2e_endpoints.py replay
    

This sweep calls every public FMPDataClient method, writes VCR cassettes under tests/e2e/vcr_cassettes/ (gitignored), and writes a JSON report to tests/e2e/reports/last-report.json. It is deselected from make test. Use it to debug model/path drift after an FMP change. The cheaper declaration probe remains pytest tests/e2e/ -m live.

Test Structure

tests/
├── __init__.py
├── conftest.py          # Shared fixtures
├── unit/                # Unit tests
└── integration/         # Integration tests

Writing Tests

  1. Fixtures: Add shared fixtures to conftest.py:

    import pytest
    
    
    @pytest.fixture
    def api_key():
        return "test_api_key"
    
    
    @pytest.fixture
    def client(api_key):
        from fmp_data import FMPDataClient
    
        return FMPDataClient(api_key=api_key)
    

  2. Test Files: Create test files with descriptive names:

    from unittest.mock import Mock
    
    from fmp_data.company.models import CompanyProfile
    
    
    def test_get_company_profile(client):
        """Test retrieving company profile."""
        client.company.client.request = Mock(return_value=[CompanyProfile(symbol="AAPL")])
        profile = client.company.get_profile("AAPL")
        assert profile.symbol == "AAPL"
    

Mocking HTTP Requests

Mock the client's internal request method in unit tests to avoid real HTTP calls:

from unittest.mock import Mock

from fmp_data.company.models import CompanyProfile


def test_api_call(client):
    # Mock API response at the client request layer
    client.company.client.request = Mock(return_value=[CompanyProfile(symbol="AAPL")])

    # Make request through business logic
    result = client.company.get_profile("AAPL")

    # Verify result
    assert result.symbol == "AAPL"

Test Coverage

We maintain high test coverage:

  • Core minimum coverage: 80% (fmp_data/lc/*, fmp_data/mcp/*, and fmp_data/cache/redis_backend.py stay omitted from that number)
  • Extras gate: 80% via uv tool run nox -s coverage_extras (or the CI extras-coverage job, required by Test-MatrixExpected). That session installs langchain / mcp / cache-redis and measures only those trees. The floor was raised from 65% after dedicated tests left about 7 points of headroom (#283).
  • Coverage report: uv run pytest --cov=fmp_data --cov-report=html
  • View report: open htmlcov/index.html

Continuous Integration

Tests run automatically on: - Every pull request - Push to main branch - Release creation Coverage is collected in a dedicated CI job separate from the test matrix.

Best Practices

  1. Test Organization:
  2. One test file per module
  3. Descriptive test names
  4. Group related tests in classes

  5. Test Data:

  6. Use fixtures for shared data
  7. Mock external API calls
  8. Use realistic test data

  9. Assertions:

  10. Be specific in assertions
  11. Test edge cases
  12. Handle exceptions properly