This document provides comprehensive information about the testing infrastructure for PlotSenseAI, including how to run tests, write new tests, and understand the testing architecture.
- Overview
- Quick Start
- Python Testing
- Frontend Testing
- Test Organization
- Running Tests
- Writing Tests
- CI/CD Integration
- Pre-commit Hooks
- Coverage Reports
- Best Practices
- Troubleshooting
PlotSenseAI uses a comprehensive testing infrastructure that includes:
- Backend: pytest with coverage tracking for Python code
- Frontend: Vitest with React Testing Library for TypeScript/React code
- CI/CD: GitHub Actions for automated testing on every push/PR
- Pre-commit hooks: Automated checks before commits
- Code quality: Linting with flake8 (Python) and ESLint (TypeScript)
Python:
pip install -e .
pip install pytest pytest-cov pytest-mockFrontend:
cd web
npm installPython:
pytestFrontend:
cd web
npm test- Test Runner: pytest 7.0+
- Coverage: pytest-cov
- Mocking: pytest-mock, unittest.mock
- Configuration:
pytest.ini,pyproject.toml,.coveragerc
The test suite is organized into three categories:
tests/
├── conftest.py # Shared fixtures and configuration
├── unit/ # Fast, isolated unit tests (mocked APIs)
│ ├── conftest.py # Unit test specific fixtures
│ ├── test_exceptions.py # Exception handling tests
│ ├── test_plot_generator.py # Plot generation unit tests
│ ├── test_explanations_unit.py # PlotExplainer unit tests (mocked)
│ └── test_suggestions_unit.py # VisualizationRecommender unit tests (mocked)
├── integration/ # Integration tests (component interaction)
│ ├── conftest.py # Integration test fixtures
│ └── (currently placeholder)
└── live/ # Live tests (real API calls)
├── conftest.py # Live test fixtures
├── test_live_suggestions.py # Real Groq/OpenAI API tests
└── test_live_explanations.py # Real Groq/OpenAI API tests
Test Coverage:
- Unit Tests: 56 tests covering core functionality with mocked APIs
- Live Tests: 5 tests requiring real API keys (Groq, OpenAI)
# Run all tests (unit and integration only, live tests skipped)
pytest
# Run only non-live tests (same as above)
pytest -m "not live"
# Run only live tests (requires GROQ_API_KEY and OPENAI_API_KEY)
pytest -m live
# Run only unit tests
pytest tests/unit
# Run with coverage
pytest --cov=plotsense --cov-report=html
# Run specific test file
pytest tests/unit/test_plot_generator.py
# Run specific test class
pytest tests/unit/test_plot_generator.py::TestPlotFunctions
# Run specific test
pytest tests/unit/test_plot_generator.py::TestPlotFunctions::test_create_scatter
# Run tests by marker
pytest -m unit # Fast unit tests only
pytest -m integration # Integration tests only
pytest -m "not live" # All tests except live (default)
pytest -m live # Only live tests
# Verbose output with details
pytest -v
# Stop on first failure
pytest -x
# Show local variables in tracebacks
pytest -l
# Run tests in parallel (requires pytest-xdist)
pytest -n autoTests are organized with markers for selective execution:
@pytest.mark.unit: Fast, isolated unit tests (mocked APIs)@pytest.mark.integration: Tests that combine multiple components@pytest.mark.e2e: End-to-end workflow tests@pytest.mark.slow: Tests that take significant time@pytest.mark.api: Tests that mock external API calls@pytest.mark.requires_api_key: Tests requiring valid credentials@pytest.mark.plotting: Tests that generate visualizations@pytest.mark.live: Live tests requiring real API keys and actual API calls (opt-in)
The tests/conftest.py file provides shared fixtures:
test_api_key: Test API key from environmentsample_dataframe: Standard 100-row DataFrame for testingsmall_dataframe: 20-row DataFrame for quick testslarge_dataframe: 1000-row DataFrame for performance testssample_suggestions: Mock visualization suggestionssimple_plot,sample_plot,scatter_plot,bar_plot: Matplotlib plot fixturesmock_groq_client: Mocked Groq API clientmock_groq_completion: Mocked Groq completion responsetemp_image_path: Temporary image file for testingreset_matplotlib: Auto-use fixture that closes all plots after each testreset_random_seed: Auto-use fixture that resets NumPy seed for reproducibility
import pytest
from plotsense import plotgen
@pytest.mark.unit
def test_scatter_plot(sample_dataframe, sample_suggestions):
"""Test scatter plot generation."""
fig = plotgen(sample_dataframe, 0, sample_suggestions)
assert fig is not None
assert len(fig.axes[0].collections) == 1- Test Runner: Vitest 2.1+
- Testing Library: React Testing Library 16.1+
- User Interactions: @testing-library/user-event
- Coverage: @vitest/coverage-v8
- Configuration:
vitest.config.ts
web/src/
├── test/
│ ├── setup.ts # Global test setup
│ └── test-utils.tsx # Custom render utilities
└── components/
└── ui/
├── button.tsx
├── button.test.tsx # Button component tests
├── card.tsx
└── card.test.tsx # Card component tests
cd web
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run with UI
npm run test:ui
# Run with coverage
npm run test:coverage
# Run specific test file
npm test -- button.test.tsx
# Run tests matching pattern
npm test -- --grep "Button"import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Button from './button'
describe('Button', () => {
it('should call onClick when clicked', async () => {
const handleClick = vi.fn()
const user = userEvent.setup()
render(<Button onClick={handleClick}>Click me</Button>)
await user.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})-
Unit Tests (
tests/unit/)- Test individual functions/components in isolation
- Fast execution (< 100ms per test)
- All external APIs are mocked
- No real API calls
- Example: Testing plot generation, exception handling
-
Integration Tests (
tests/integration/)- Test interaction between components
- May use mocked external services
- Example: Testing recommendation → plot generation flow
-
Live Tests (
tests/live/)- Test with real API calls (Groq, OpenAI)
- Require valid API credentials (GROQ_API_KEY, OPENAI_API_KEY)
- Skipped by default with
pytest - Run explicitly with
pytest -m live - Example: Real end-to-end workflow with actual API calls
# Python: Run fast unit tests during development
pytest tests/unit
# Python: Run all non-live tests
pytest -m "not live"
# Python: Watch mode (requires pytest-watch)
ptw tests/unit
# Frontend: Run in watch mode
cd web && npm test -- --watch# Run pre-commit hooks manually
pre-commit run --all-files
# Or just run tests
pytest -v
cd web && npm testTests run automatically on:
- Every push to
main,dev, orfeature/*branches - Every pull request to
mainordev
See .github/workflows/ for CI configuration.
import pytest
from plotsense.module import function_to_test
class TestFeatureName:
"""Test suite for feature X."""
@pytest.mark.unit
def test_basic_functionality(self, sample_dataframe):
"""Test basic use case (runs by default)."""
result = function_to_test(sample_dataframe)
assert result is not None
@pytest.mark.integration
def test_integration_scenario(self, mock_groq_client):
"""Test integration with mocked external service."""
# Test implementation
pass
@pytest.mark.live
def test_real_api_call(self, sample_dataframe):
"""Test with real API call (opt-in via pytest -m live)."""
# This requires real API keys
pass
def test_error_handling(self):
"""Test that errors are handled properly."""
with pytest.raises(ValueError):
function_to_test(invalid_input)Key Points:
- By default, unit and integration tests run:
pytest - Live tests are skipped by default
- To run live tests:
pytest -m live(requires API keys) - To exclude live tests explicitly:
pytest -m "not live"
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Component from './Component'
describe('Component', () => {
it('should render correctly', () => {
render(<Component />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should handle user interaction', async () => {
const user = userEvent.setup()
render(<Component />)
await user.click(screen.getByRole('button'))
expect(screen.getByText('Updated')).toBeInTheDocument()
})
})Python Tests (.github/workflows/python-tests.yml):
- Runs on: Ubuntu, Windows, macOS
- Python versions: 3.9, 3.10, 3.11, 3.12
- Includes: Linting, testing, coverage reporting
Frontend Tests (.github/workflows/frontend-tests.yml):
- Runs on: Ubuntu
- Node version: 20
- Includes: Linting, type checking, testing, build verification
Add these to GitHub repository secrets:
GROQ_API_KEY: For testing API integrations (optional - tests use mocks)CODECOV_TOKEN: For coverage reporting (optional)
pip install pre-commit
pre-commit install- File checks: Trailing whitespace, end-of-file, large files
- Python: flake8 linting, security checks with bandit
- Frontend: ESLint, TypeScript type checking
- Tests: Fast unit tests run before commit
- Commit messages: Conventional commit format
git commit --no-verifyPython:
pytest --cov=plotsense --cov-report=html --cov-report=term
open htmlcov/index.html # View HTML reportFrontend:
cd web
npm run test:coverage
open coverage/index.html # View HTML report- Minimum: 70% line and branch coverage
- Target: 80%+ coverage for critical modules
- Exclusions: Test files, configuration,
__repr__methods
- Write tests first (TDD) when fixing bugs
- Keep tests focused: One concept per test
- Use descriptive names:
test_scatter_plot_with_missing_data - Avoid test interdependence: Each test should run independently
- Mock external services: Don't make real API calls in tests
- Use fixtures: Leverage
conftest.pyfixtures - Mark tests appropriately: Use
@pytest.mark.*decorators - Test edge cases: Empty data, NaN values, invalid inputs
- Clean up resources: Close figures with
plt.close(fig) - Use parametrize: Test multiple inputs efficiently
@pytest.mark.parametrize("plot_type", ["scatter", "bar", "hist"])
def test_plot_types(plot_type, sample_dataframe):
# Test implementation
pass- Query by role/label: Prefer
getByRole,getByLabelText - Test user behavior: Use
userEventfor interactions - Avoid implementation details: Don't test internal state
- Test accessibility: Ensure proper ARIA attributes
- Use data-testid sparingly: Only when semantic queries fail
Issue: Tests fail with "No module named 'plotsense'"
# Solution: Install package in editable mode
pip install -e .Issue: Frontend tests fail with "Cannot find module '@/...'"
# Solution: Check path alias in vite.config.ts and tsconfig.jsonIssue: Matplotlib tests fail on CI
# Solution: Ensure non-interactive backend in test
matplotlib.use('Agg')Issue: Tests are too slow
# Solution: Run fast tests only
pytest -m "not slow"Issue: Coverage report not generated
# Solution: Install coverage tools
pip install pytest-covPython:
# Drop into debugger on failure
pytest --pdb
# Show print statements
pytest -sFrontend:
# Run with UI for debugging
npm run test:uiWhen adding new features:
- Write tests for new functionality
- Place unit tests in
tests/unit/with@pytest.mark.unitor no marker - Place integration tests in
tests/integration/with@pytest.mark.integration - Place live API tests in
tests/live/with@pytest.mark.live - Ensure all tests pass:
pytest- runs unit and integration testspytest -m live- runs live tests (requires API keys)
- Check coverage: Aim for 80%+ on new code
pytest --cov=plotsense --cov-report=html
- Run pre-commit hooks:
pre-commit run --all-files - Update this document if adding new test patterns
For questions or issues with testing, please open an issue on GitHub.