-
-
Notifications
You must be signed in to change notification settings - Fork 450
Testing Guide for Contributors
Welcome to the Calibre-Web Automated testing guide! This page will help you understand our testing system and how to contribute tests for new features or bug fixes.
- Why We Test
- Getting Started
- Running Tests
- Test Categories
- Writing Your First Test
- Common Testing Patterns
- Testing Checklist for PRs
- Troubleshooting
CWA is a complex application with:
- Multiple background services (s6-overlay)
- Three separate SQLite databases
- 27+ ebook import formats
- Docker deployment across different architectures
- Integration with Calibre CLI tools
Manual testing is time-consuming and error-prone. Automated tests help us:
- Catch bugs before they reach users
- Prevent regressions when adding new features
- Give contributors confidence their changes work
- Speed up the review process
- Serve as living documentation
From the project root directory:
pip install -r requirements-dev.txtThis installs pytest and related testing tools.
Run a quick smoke test to verify everything works:
pytest tests/smoke/test_smoke.py::test_smoke_suite_itself -vYou should see: ✅ PASSED
tests/
├── conftest.py # Shared fixtures (automatic)
├── smoke/ # Fast sanity checks (~30 seconds)
│ └── test_smoke.py
├── unit/ # Isolated component tests (~2 minutes)
│ ├── test_cwa_db.py
│ └── test_*.py
├── integration/ # Multi-component tests (~10 minutes)
│ └── (coming soon)
└── e2e/ # Full workflow tests (~30 minutes)
└── (coming soon)
# Run ALL smoke tests (do this before committing!)
pytest tests/smoke/ -v
# Run ALL unit tests
pytest tests/unit/ -v
# Run a specific test file
pytest tests/unit/test_cwa_db.py -v
# Run a specific test function
pytest tests/unit/test_cwa_db.py::TestCWADBInitialization::test_database_creates_successfully -v
# Run tests matching a pattern
pytest -k "database" -v
# Run with coverage report
pytest tests/unit/ --cov=scripts --cov=cps --cov-report=html
# Then open: htmlcov/index.htmltests/smoke/test_smoke.py::test_python_version PASSED [10%]
tests/smoke/test_smoke.py::test_flask_app_can_be_imported PASSED [20%]- ✅ PASSED - Test succeeded
- ❌ FAILED - Test failed (check the error message)
- ⏭️ SKIPPED - Test was skipped (usually requires Docker/Calibre)
Location: tests/smoke/
Run Time: <30 seconds
Purpose: Verify basic functionality isn't broken
When to add smoke tests:
- Core application startup
- Database connectivity
- Required binaries are present
- Critical configuration loading
Example:
@pytest.mark.smoke
def test_app_can_start():
"""Verify Flask app initializes without errors."""
from cps import create_app
app = create_app()
assert app is not NoneLocation: tests/unit/
Run Time: ~2 minutes
Purpose: Test individual functions in isolation
When to add unit tests:
- New utility functions
- Data validation logic
- Format detection/parsing
- Database operations
- File handling logic
Example:
@pytest.mark.unit
def test_file_format_detection():
"""Verify EPUB files are correctly identified."""
from scripts.ingest_processor import is_supported_format
assert is_supported_format("book.epub") is True
assert is_supported_format("book.txt") is True
assert is_supported_format("book.exe") is FalseLocation: tests/integration/
Run Time: ~10 minutes
Purpose: Test multiple components working together
When to add integration tests:
- Ingest pipeline workflows
- Database + file system interactions
- Calibre CLI integration
- OAuth/LDAP authentication flows
Example:
@pytest.mark.integration
def test_book_import_workflow(temp_library, sample_epub):
"""Verify complete book import process."""
result = import_book(sample_epub, temp_library)
assert result['success'] is True
assert book_exists_in_library(temp_library, result['book_id'])Location: tests/e2e/
Run Time: ~30 minutes
Purpose: Test complete user workflows in Docker
When to add E2E tests:
- Major feature releases
- Multi-service interactions
- Docker-specific behavior
- Network share mode testing
Ask yourself:
- Does this test require Docker? → E2E
- Does it test multiple components? → Integration
- Does it test one function? → Unit
- Does it verify basic functionality? → Smoke
Create a new file following the naming convention:
# Unit test example
touch tests/unit/test_my_feature.py"""
Unit tests for my new feature.
Brief description of what this module tests.
"""
import pytest
@pytest.mark.unit
class TestMyFeature:
"""Test suite for MyFeature functionality."""
def test_feature_with_valid_input(self):
"""Test that feature handles valid input correctly."""
# Arrange
input_data = "valid input"
# Act
result = my_function(input_data)
# Assert
assert result is not None
assert result == "expected output"
def test_feature_with_invalid_input(self):
"""Test that feature handles invalid input gracefully."""
with pytest.raises(ValueError):
my_function(None)Instead of creating test data manually, use fixtures from conftest.py:
def test_with_database(temp_cwa_db):
"""The temp_cwa_db fixture is automatically available."""
# Test uses temporary database that's cleaned up automatically
temp_cwa_db.insert_import_log(1, "Test Book", "EPUB", "/path")
assert temp_cwa_db.get_total_imports() == 1Available fixtures:
-
temp_dir- Temporary directory -
temp_cwa_db- Temporary CWA database -
temp_library_dir- Temporary Calibre library -
sample_book_data- Sample book metadata -
sample_user_data- Sample user data -
mock_calibre_tools- Mocked Calibre binaries
pytest tests/unit/test_my_feature.py -vpytest tests/unit/test_my_feature.py --cov=my_module --cov-report=termAim for >80% coverage on new code.
def test_database_insert(temp_cwa_db):
"""Test inserting data into database."""
# Insert
temp_cwa_db.insert_import_log(
book_id=1,
title="Test Book",
format="EPUB",
file_path="/path/to/book.epub"
)
# Query and verify
logs = temp_cwa_db.query_import_logs(limit=1)
assert len(logs) == 1
assert logs[0]['title'] == "Test Book"def test_file_processing(tmp_path):
"""Test file is processed correctly."""
# Create test file
test_file = tmp_path / "test.epub"
test_file.write_text("epub content")
# Process
result = process_file(str(test_file))
# Verify
assert result['success'] is True
assert test_file.exists() # Or doesn't exist, depending on logicdef test_calibre_import(mock_calibre_tools, sample_epub):
"""Test book import using Calibre."""
# mock_calibre_tools automatically mocks subprocess.run
result = import_with_calibredb(sample_epub)
assert result is True
mock_calibre_tools['calibredb'].assert_called_once()@pytest.mark.parametrize("format,expected", [
("epub", True),
("mobi", True),
("pdf", True),
("exe", False),
("txt", True),
])
def test_format_detection(format, expected):
"""Test format detection for multiple file types."""
result = is_supported_format(f"book.{format}")
assert result == expecteddef test_handles_missing_file_gracefully():
"""Test that missing files don't crash the app."""
result = process_file("/nonexistent/file.epub")
assert result['success'] is False
assert 'error' in result
assert "not found" in result['error'].lower()@pytest.mark.timeout(10) # Fail if takes >10 seconds
def test_background_task_completes():
"""Test background task runs to completion."""
import time
task = start_background_task()
# Wait for completion with timeout
max_wait = 5
start = time.time()
while not task.is_complete() and (time.time() - start) < max_wait:
time.sleep(0.1)
assert task.is_complete()
assert task.success is TrueBefore submitting a pull request, verify:
- New features have corresponding tests
- Bug fixes have regression tests
- At least one test per new function/method
- All smoke tests pass:
pytest tests/smoke/ -v - All unit tests pass:
pytest tests/unit/ -v - New tests pass individually
- Tests pass in CI/CD pipeline
- New code has >70% test coverage
- Critical functions have >80% coverage
- Check with:
pytest --cov=. --cov-report=term
- Tests have descriptive names
- Tests have docstrings explaining what they verify
- Tests use fixtures instead of manual setup
- Tests clean up after themselves (automatic with fixtures)
- Tests are independent (don't rely on other tests)
- Complex test logic is commented
- Test file has module-level docstring
- Non-obvious test behavior is explained
# Make sure you're in the project root
cd /app/calibre-web-automated
# Reinstall dependencies
pip install -r requirements.txt
pip install -r requirements-dev.txtThis usually means:
- Missing dependency in
requirements-dev.txt - Docker-specific behavior (test needs
@pytest.mark.requires_docker) - Calibre tools not available (test needs
@pytest.mark.requires_calibre)
# Clear lock files
rm /tmp/*.lock
# Tests should use temp_cwa_db fixture to avoid conflicts# Run tests in parallel
pytest -n auto tests/unit/
# Skip slow tests during development
pytest -m "not slow" tests/Tests should use tmp_path or temp_dir fixtures, not system directories:
# ❌ Bad - uses system directory
def test_bad():
with open('/config/test.txt', 'w') as f:
f.write('test')
# ✅ Good - uses temporary directory
def test_good(tmp_path):
test_file = tmp_path / "test.txt"
test_file.write_text('test')Common fixtures are in tests/conftest.py and automatically available. If you see this error:
- Check spelling of fixture name
- Verify fixture exists in
conftest.py - Check that test file is in
tests/directory
# Start container
docker compose up -d
# Run tests inside container
docker exec -it calibre-web-automated pytest tests/smoke/ -vAdd to tests/conftest.py:
@pytest.fixture
def my_custom_fixture():
"""Provide custom test data."""
# Setup
data = create_test_data()
yield data
# Cleanup (optional)
cleanup_test_data(data)Then use in tests:
def test_with_custom_fixture(my_custom_fixture):
assert my_custom_fixture is not Nonedef test_with_mocked_api(requests_mock):
"""Test API integration with mocked responses."""
# Mock API response
requests_mock.get(
'https://api.example.com/metadata',
json={'title': 'Mocked Book'}
)
# Test function that calls API
result = fetch_metadata('123')
assert result['title'] == 'Mocked Book'from freezegun import freeze_time
@freeze_time("2024-01-01 12:00:00")
def test_scheduled_task():
"""Test task runs at scheduled time."""
# Code thinks it's 2024-01-01 at noon
result = should_run_daily_task()
assert result is True-
Full documentation: See
TESTING_STRATEGY.mdin project root -
Example tests: Browse
tests/smoke/andtests/unit/directories - Ask questions: Discord server: https://discord.gg/EjgSeek94R
- Report issues: GitHub Issues
We appreciate test contributions! Here's how to help:
- Pick an untested area: Check coverage report to find gaps
- Write tests: Follow patterns in this guide
- Run tests locally: Verify they pass
- Submit PR: Include tests with your feature/bugfix
- Respond to feedback: Reviewers may suggest improvements
Good first test contributions:
- Add missing unit tests for utility functions
- Add parameterized tests for format detection
- Add edge case tests for existing functions
- Improve test coverage of core modules
- Critical modules (ingest_processor, cwa_db, helper): 80%+
- Core application: 70%+
- Overall project: 50%+
Check current coverage:
pytest --cov=cps --cov=scripts --cov-report=term --cov-report=htmlView detailed report: Open htmlcov/index.html in your browser
# Most common commands
pytest tests/smoke/ -v # Fast sanity check
pytest tests/unit/ -v # Unit tests
pytest -k "test_name" -v # Run specific test
pytest --cov=. --cov-report=html # Coverage report
pytest -n auto # Parallel execution
pytest --lf # Run last failed tests
pytest -x # Stop on first failure
pytest -vv # Extra verboseThank you for contributing to CWA's test suite! 🎉 Every test makes the project more reliable and easier to maintain.