Version headers/footers updated to 3.1.0-dev: - docs/features/BOOTSTRAP_SKILL_TECHNICAL.md (was 2.8.0-dev) - docs/reference/API_REFERENCE.md (was 2.7.0) - docs/reference/CODE_QUALITY.md (was 2.7.0) - docs/guides/TESTING_GUIDE.md (was 2.7.0) - docs/guides/MIGRATION_GUIDE.md (was 2.7.0, historical tables untouched) MCP tool count 18 → 26: - docs/guides/MCP_SETUP.md - docs/guides/TESTING_GUIDE.md - docs/reference/CODE_QUALITY.md - docs/reference/CLAUDE_INTEGRATION.md - docs/integrations/CLINE.md - docs/strategy/INTEGRATION_STRATEGY.md Test count 700+/1200+ → 1,880+: - docs/guides/MCP_SETUP.md - docs/guides/TESTING_GUIDE.md - docs/reference/CODE_QUALITY.md - docs/reference/CLAUDE_INTEGRATION.md - docs/features/HOW_TO_GUIDES.md - docs/blog/UNIVERSAL_RAG_PREPROCESSOR.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
935 lines
21 KiB
Markdown
935 lines
21 KiB
Markdown
# Testing Guide
|
|
|
|
**Version:** 3.1.0-dev
|
|
**Last Updated:** 2026-02-18
|
|
**Test Count:** 1,880+ tests
|
|
**Coverage:** >85%
|
|
**Status:** ✅ Production Ready
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
Skill Seekers has comprehensive test coverage with **1,880+ tests** spanning unit tests, integration tests, end-to-end tests, and MCP integration tests. This guide covers everything you need to know about testing in the project.
|
|
|
|
**Test Philosophy:**
|
|
- **Never skip tests** - All tests must pass before commits
|
|
- **Test-driven development** - Write tests first when possible
|
|
- **Comprehensive coverage** - >80% code coverage minimum
|
|
- **Fast feedback** - Unit tests run in seconds
|
|
- **CI/CD integration** - Automated testing on every commit
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
### Running All Tests
|
|
|
|
```bash
|
|
# Install package with dev dependencies
|
|
pip install -e ".[all-llms,dev]"
|
|
|
|
# Run all tests
|
|
pytest tests/ -v
|
|
|
|
# Run with coverage
|
|
pytest tests/ --cov=src/skill_seekers --cov-report=html
|
|
|
|
# View coverage report
|
|
open htmlcov/index.html
|
|
```
|
|
|
|
**Expected Output:**
|
|
```
|
|
============================== test session starts ===============================
|
|
platform linux -- Python 3.11.7, pytest-8.4.2, pluggy-1.5.0 -- /usr/bin/python3
|
|
cachedir: .pytest_cache
|
|
rootdir: /path/to/Skill_Seekers
|
|
configfile: pyproject.toml
|
|
plugins: asyncio-0.24.0, cov-7.0.0
|
|
collected 1215 items
|
|
|
|
tests/test_scraper_features.py::test_detect_language PASSED [ 1%]
|
|
tests/test_scraper_features.py::test_smart_categorize PASSED [ 2%]
|
|
...
|
|
============================== 1215 passed in 45.23s ==============================
|
|
```
|
|
|
|
---
|
|
|
|
## Test Structure
|
|
|
|
### Directory Layout
|
|
|
|
```
|
|
tests/
|
|
├── test_*.py # Unit tests (800+ tests)
|
|
├── test_*_integration.py # Integration tests (300+ tests)
|
|
├── test_*_e2e.py # End-to-end tests (100+ tests)
|
|
├── test_mcp*.py # MCP tests (63 tests)
|
|
├── fixtures/ # Test fixtures and data
|
|
│ ├── configs/ # Test configurations
|
|
│ ├── html/ # Sample HTML files
|
|
│ ├── pdfs/ # Sample PDF files
|
|
│ └── repos/ # Sample repository structures
|
|
└── conftest.py # Shared pytest fixtures
|
|
```
|
|
|
|
### Test File Naming Conventions
|
|
|
|
| Pattern | Purpose | Example |
|
|
|---------|---------|---------|
|
|
| `test_*.py` | Unit tests | `test_doc_scraper.py` |
|
|
| `test_*_integration.py` | Integration tests | `test_unified_integration.py` |
|
|
| `test_*_e2e.py` | End-to-end tests | `test_install_e2e.py` |
|
|
| `test_mcp*.py` | MCP server tests | `test_mcp_fastmcp.py` |
|
|
|
|
---
|
|
|
|
## Test Categories
|
|
|
|
### 1. Unit Tests (800+ tests)
|
|
|
|
Test individual functions and classes in isolation.
|
|
|
|
#### Example: Testing Language Detection
|
|
|
|
```python
|
|
# tests/test_scraper_features.py
|
|
|
|
def test_detect_language():
|
|
"""Test code language detection from CSS classes."""
|
|
from skill_seekers.cli.doc_scraper import detect_language
|
|
|
|
# Test Python detection
|
|
html = '<code class="language-python">def foo():</code>'
|
|
assert detect_language(html) == 'python'
|
|
|
|
# Test JavaScript detection
|
|
html = '<code class="lang-js">const x = 1;</code>'
|
|
assert detect_language(html) == 'javascript'
|
|
|
|
# Test heuristics fallback
|
|
html = '<code>def foo():</code>'
|
|
assert detect_language(html) == 'python'
|
|
|
|
# Test unknown language
|
|
html = '<code>random text</code>'
|
|
assert detect_language(html) == 'unknown'
|
|
```
|
|
|
|
#### Running Unit Tests
|
|
|
|
```bash
|
|
# All unit tests
|
|
pytest tests/test_*.py -v
|
|
|
|
# Specific test file
|
|
pytest tests/test_scraper_features.py -v
|
|
|
|
# Specific test function
|
|
pytest tests/test_scraper_features.py::test_detect_language -v
|
|
|
|
# With output
|
|
pytest tests/test_scraper_features.py -v -s
|
|
```
|
|
|
|
### 2. Integration Tests (300+ tests)
|
|
|
|
Test multiple components working together.
|
|
|
|
#### Example: Testing Multi-Source Scraping
|
|
|
|
```python
|
|
# tests/test_unified_integration.py
|
|
|
|
def test_unified_scraping_integration(tmp_path):
|
|
"""Test docs + GitHub + PDF unified scraping."""
|
|
from skill_seekers.cli.unified_scraper import unified_scrape
|
|
|
|
# Create unified config
|
|
config = {
|
|
'name': 'test-unified',
|
|
'sources': {
|
|
'documentation': {
|
|
'type': 'docs',
|
|
'base_url': 'https://docs.example.com',
|
|
'selectors': {'main_content': 'article'}
|
|
},
|
|
'github': {
|
|
'type': 'github',
|
|
'repo_url': 'https://github.com/org/repo',
|
|
'analysis_depth': 'basic'
|
|
},
|
|
'pdf': {
|
|
'type': 'pdf',
|
|
'pdf_path': 'tests/fixtures/pdfs/sample.pdf'
|
|
}
|
|
}
|
|
}
|
|
|
|
# Run unified scraping
|
|
result = unified_scrape(
|
|
config=config,
|
|
output_dir=tmp_path / 'output'
|
|
)
|
|
|
|
# Verify all sources processed
|
|
assert result['success']
|
|
assert len(result['sources']) == 3
|
|
assert 'documentation' in result['sources']
|
|
assert 'github' in result['sources']
|
|
assert 'pdf' in result['sources']
|
|
|
|
# Verify skill created
|
|
skill_path = tmp_path / 'output' / 'test-unified' / 'SKILL.md'
|
|
assert skill_path.exists()
|
|
```
|
|
|
|
#### Running Integration Tests
|
|
|
|
```bash
|
|
# All integration tests
|
|
pytest tests/test_*_integration.py -v
|
|
|
|
# Specific integration test
|
|
pytest tests/test_unified_integration.py -v
|
|
|
|
# With coverage
|
|
pytest tests/test_*_integration.py --cov=src/skill_seekers
|
|
```
|
|
|
|
### 3. End-to-End Tests (100+ tests)
|
|
|
|
Test complete user workflows from start to finish.
|
|
|
|
#### Example: Testing Complete Install Workflow
|
|
|
|
```python
|
|
# tests/test_install_e2e.py
|
|
|
|
def test_install_workflow_end_to_end(tmp_path):
|
|
"""Test complete install workflow: fetch → scrape → package."""
|
|
from skill_seekers.cli.install_skill import install_skill
|
|
|
|
# Run complete workflow
|
|
result = install_skill(
|
|
config_name='react',
|
|
target='markdown', # No API key needed
|
|
output_dir=tmp_path,
|
|
enhance=False, # Skip AI enhancement
|
|
upload=False, # Don't upload
|
|
force=True # Skip confirmations
|
|
)
|
|
|
|
# Verify workflow completed
|
|
assert result['success']
|
|
assert result['package_path'].endswith('.zip')
|
|
|
|
# Verify package contents
|
|
import zipfile
|
|
with zipfile.ZipFile(result['package_path']) as z:
|
|
files = z.namelist()
|
|
assert 'SKILL.md' in files
|
|
assert 'metadata.json' in files
|
|
assert any(f.startswith('references/') for f in files)
|
|
```
|
|
|
|
#### Running E2E Tests
|
|
|
|
```bash
|
|
# All E2E tests
|
|
pytest tests/test_*_e2e.py -v
|
|
|
|
# Specific E2E test
|
|
pytest tests/test_install_e2e.py -v
|
|
|
|
# E2E tests can be slow, run in parallel
|
|
pytest tests/test_*_e2e.py -v -n auto
|
|
```
|
|
|
|
### 4. MCP Tests (63 tests)
|
|
|
|
Test MCP server and all 26 MCP tools.
|
|
|
|
#### Example: Testing MCP Tool
|
|
|
|
```python
|
|
# tests/test_mcp_fastmcp.py
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_list_configs():
|
|
"""Test list_configs MCP tool."""
|
|
from skill_seekers.mcp.server_fastmcp import app
|
|
|
|
# Call list_configs tool
|
|
result = await app.call_tool('list_configs', {})
|
|
|
|
# Verify result structure
|
|
assert 'configs' in result
|
|
assert isinstance(result['configs'], list)
|
|
assert len(result['configs']) > 0
|
|
|
|
# Verify config structure
|
|
config = result['configs'][0]
|
|
assert 'name' in config
|
|
assert 'description' in config
|
|
assert 'category' in config
|
|
```
|
|
|
|
#### Running MCP Tests
|
|
|
|
```bash
|
|
# All MCP tests
|
|
pytest tests/test_mcp*.py -v
|
|
|
|
# FastMCP server tests
|
|
pytest tests/test_mcp_fastmcp.py -v
|
|
|
|
# HTTP transport tests
|
|
pytest tests/test_server_fastmcp_http.py -v
|
|
|
|
# With async support
|
|
pytest tests/test_mcp*.py -v --asyncio-mode=auto
|
|
```
|
|
|
|
---
|
|
|
|
## Test Markers
|
|
|
|
### Available Markers
|
|
|
|
Pytest markers organize and filter tests:
|
|
|
|
```python
|
|
# Mark slow tests
|
|
@pytest.mark.slow
|
|
def test_large_documentation_scraping():
|
|
"""Slow test - takes 5+ minutes."""
|
|
pass
|
|
|
|
# Mark async tests
|
|
@pytest.mark.asyncio
|
|
async def test_async_scraping():
|
|
"""Async test using asyncio."""
|
|
pass
|
|
|
|
# Mark integration tests
|
|
@pytest.mark.integration
|
|
def test_multi_component_workflow():
|
|
"""Integration test."""
|
|
pass
|
|
|
|
# Mark E2E tests
|
|
@pytest.mark.e2e
|
|
def test_end_to_end_workflow():
|
|
"""End-to-end test."""
|
|
pass
|
|
```
|
|
|
|
### Running Tests by Marker
|
|
|
|
```bash
|
|
# Skip slow tests (default for fast feedback)
|
|
pytest tests/ -m "not slow"
|
|
|
|
# Run only slow tests
|
|
pytest tests/ -m slow
|
|
|
|
# Run only async tests
|
|
pytest tests/ -m asyncio
|
|
|
|
# Run integration + E2E tests
|
|
pytest tests/ -m "integration or e2e"
|
|
|
|
# Run everything except slow tests
|
|
pytest tests/ -v -m "not slow"
|
|
```
|
|
|
|
---
|
|
|
|
## Writing Tests
|
|
|
|
### Test Structure Pattern
|
|
|
|
Follow the **Arrange-Act-Assert** pattern:
|
|
|
|
```python
|
|
def test_scrape_single_page():
|
|
"""Test scraping a single documentation page."""
|
|
# Arrange: Set up test data and mocks
|
|
base_url = 'https://docs.example.com/intro'
|
|
config = {
|
|
'name': 'test',
|
|
'selectors': {'main_content': 'article'}
|
|
}
|
|
|
|
# Act: Execute the function under test
|
|
result = scrape_page(base_url, config)
|
|
|
|
# Assert: Verify the outcome
|
|
assert result['title'] == 'Introduction'
|
|
assert 'content' in result
|
|
assert result['url'] == base_url
|
|
```
|
|
|
|
### Using Fixtures
|
|
|
|
#### Shared Fixtures (conftest.py)
|
|
|
|
```python
|
|
# tests/conftest.py
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
@pytest.fixture
|
|
def temp_output_dir(tmp_path):
|
|
"""Create temporary output directory."""
|
|
output_dir = tmp_path / 'output'
|
|
output_dir.mkdir()
|
|
return output_dir
|
|
|
|
@pytest.fixture
|
|
def sample_config():
|
|
"""Provide sample configuration."""
|
|
return {
|
|
'name': 'test-framework',
|
|
'description': 'Test configuration',
|
|
'base_url': 'https://docs.example.com',
|
|
'selectors': {
|
|
'main_content': 'article',
|
|
'title': 'h1'
|
|
}
|
|
}
|
|
|
|
@pytest.fixture
|
|
def sample_html():
|
|
"""Provide sample HTML content."""
|
|
return '''
|
|
<html>
|
|
<body>
|
|
<h1>Test Page</h1>
|
|
<article>
|
|
<p>This is test content.</p>
|
|
<pre><code class="language-python">def foo(): pass</code></pre>
|
|
</article>
|
|
</body>
|
|
</html>
|
|
'''
|
|
```
|
|
|
|
#### Using Fixtures in Tests
|
|
|
|
```python
|
|
def test_with_fixtures(temp_output_dir, sample_config, sample_html):
|
|
"""Test using multiple fixtures."""
|
|
# Fixtures are automatically injected
|
|
assert temp_output_dir.exists()
|
|
assert sample_config['name'] == 'test-framework'
|
|
assert '<html>' in sample_html
|
|
```
|
|
|
|
### Mocking External Dependencies
|
|
|
|
#### Mocking HTTP Requests
|
|
|
|
```python
|
|
from unittest.mock import patch, Mock
|
|
|
|
@patch('requests.get')
|
|
def test_scrape_with_mock(mock_get):
|
|
"""Test scraping with mocked HTTP requests."""
|
|
# Mock successful response
|
|
mock_response = Mock()
|
|
mock_response.status_code = 200
|
|
mock_response.text = '<html><body>Test</body></html>'
|
|
mock_get.return_value = mock_response
|
|
|
|
# Run test
|
|
result = scrape_page('https://example.com')
|
|
|
|
# Verify mock was called
|
|
mock_get.assert_called_once_with('https://example.com')
|
|
assert result['content'] == 'Test'
|
|
```
|
|
|
|
#### Mocking File System
|
|
|
|
```python
|
|
from unittest.mock import mock_open, patch
|
|
|
|
def test_read_config_with_mock():
|
|
"""Test config reading with mocked file system."""
|
|
mock_data = '{"name": "test", "base_url": "https://example.com"}'
|
|
|
|
with patch('builtins.open', mock_open(read_data=mock_data)):
|
|
config = read_config('config.json')
|
|
|
|
assert config['name'] == 'test'
|
|
assert config['base_url'] == 'https://example.com'
|
|
```
|
|
|
|
### Testing Exceptions
|
|
|
|
```python
|
|
import pytest
|
|
|
|
def test_invalid_config_raises_error():
|
|
"""Test that invalid config raises ValueError."""
|
|
from skill_seekers.cli.config_validator import validate_config
|
|
|
|
invalid_config = {'name': 'test'} # Missing required fields
|
|
|
|
with pytest.raises(ValueError, match="Missing required field"):
|
|
validate_config(invalid_config)
|
|
```
|
|
|
|
### Parametrized Tests
|
|
|
|
Test multiple inputs efficiently:
|
|
|
|
```python
|
|
@pytest.mark.parametrize('input_html,expected_lang', [
|
|
('<code class="language-python">def foo():</code>', 'python'),
|
|
('<code class="lang-js">const x = 1;</code>', 'javascript'),
|
|
('<code class="language-rust">fn main() {}</code>', 'rust'),
|
|
('<code>unknown code</code>', 'unknown'),
|
|
])
|
|
def test_language_detection_parametrized(input_html, expected_lang):
|
|
"""Test language detection with multiple inputs."""
|
|
from skill_seekers.cli.doc_scraper import detect_language
|
|
|
|
assert detect_language(input_html) == expected_lang
|
|
```
|
|
|
|
---
|
|
|
|
## Coverage Analysis
|
|
|
|
### Generating Coverage Reports
|
|
|
|
```bash
|
|
# Terminal coverage report
|
|
pytest tests/ --cov=src/skill_seekers --cov-report=term
|
|
|
|
# HTML coverage report (recommended)
|
|
pytest tests/ --cov=src/skill_seekers --cov-report=html
|
|
|
|
# XML coverage report (for CI/CD)
|
|
pytest tests/ --cov=src/skill_seekers --cov-report=xml
|
|
|
|
# Combined report
|
|
pytest tests/ --cov=src/skill_seekers --cov-report=term --cov-report=html
|
|
```
|
|
|
|
### Understanding Coverage Reports
|
|
|
|
**Terminal Output:**
|
|
```
|
|
Name Stmts Miss Cover
|
|
-----------------------------------------------------------------
|
|
src/skill_seekers/__init__.py 8 0 100%
|
|
src/skill_seekers/cli/doc_scraper.py 420 35 92%
|
|
src/skill_seekers/cli/github_scraper.py 310 20 94%
|
|
src/skill_seekers/cli/adaptors/claude.py 125 5 96%
|
|
-----------------------------------------------------------------
|
|
TOTAL 3500 280 92%
|
|
```
|
|
|
|
**HTML Report:**
|
|
- Green lines: Covered by tests
|
|
- Red lines: Not covered
|
|
- Yellow lines: Partially covered (branches)
|
|
|
|
### Improving Coverage
|
|
|
|
```bash
|
|
# Find untested code
|
|
pytest tests/ --cov=src/skill_seekers --cov-report=html
|
|
open htmlcov/index.html
|
|
|
|
# Click on files with low coverage (red)
|
|
# Identify untested lines
|
|
# Write tests for uncovered code
|
|
```
|
|
|
|
**Example: Adding Missing Tests**
|
|
|
|
```python
|
|
# Coverage report shows line 145 in doc_scraper.py is uncovered
|
|
# Line 145: return "unknown" # Fallback for unknown languages
|
|
|
|
# Add test for this branch
|
|
def test_detect_language_unknown():
|
|
"""Test fallback to 'unknown' for unrecognized code."""
|
|
html = '<code>completely random text</code>'
|
|
assert detect_language(html) == 'unknown'
|
|
```
|
|
|
|
---
|
|
|
|
## CI/CD Testing
|
|
|
|
### GitHub Actions Integration
|
|
|
|
Tests run automatically on every commit and pull request.
|
|
|
|
#### Workflow Configuration
|
|
|
|
```yaml
|
|
# .github/workflows/ci.yml
|
|
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main, development]
|
|
pull_request:
|
|
branches: [main, development]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest]
|
|
python-version: ['3.10', '3.11', '3.12', '3.13']
|
|
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v4
|
|
with:
|
|
python-version: ${{ matrix.python-version }}
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
pip install -e ".[all-llms,dev]"
|
|
|
|
- name: Run tests
|
|
run: |
|
|
pytest tests/ -v --cov=src/skill_seekers --cov-report=xml
|
|
|
|
- name: Upload coverage
|
|
uses: codecov/codecov-action@v3
|
|
with:
|
|
file: ./coverage.xml
|
|
fail_ci_if_error: true
|
|
```
|
|
|
|
### CI Matrix Testing
|
|
|
|
Tests run across:
|
|
- **2 operating systems:** Ubuntu + macOS
|
|
- **4 Python versions:** 3.10, 3.11, 3.12, 3.13
|
|
- **Total:** 8 test matrix configurations
|
|
|
|
**Why Matrix Testing:**
|
|
- Ensures cross-platform compatibility
|
|
- Catches Python version-specific issues
|
|
- Validates against multiple environments
|
|
|
|
### Coverage Reporting
|
|
|
|
Coverage is uploaded to Codecov for tracking:
|
|
|
|
```bash
|
|
# Generate XML coverage report
|
|
pytest tests/ --cov=src/skill_seekers --cov-report=xml
|
|
|
|
# Upload to Codecov (in CI)
|
|
codecov -f coverage.xml
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Testing
|
|
|
|
### Measuring Test Performance
|
|
|
|
```bash
|
|
# Show slowest 10 tests
|
|
pytest tests/ --durations=10
|
|
|
|
# Show all test durations
|
|
pytest tests/ --durations=0
|
|
|
|
# Profile test execution
|
|
pytest tests/ --profile
|
|
```
|
|
|
|
**Sample Output:**
|
|
```
|
|
========== slowest 10 durations ==========
|
|
12.45s call tests/test_unified_integration.py::test_large_docs
|
|
8.23s call tests/test_github_scraper.py::test_full_repo_analysis
|
|
5.67s call tests/test_pdf_scraper.py::test_ocr_extraction
|
|
3.45s call tests/test_mcp_fastmcp.py::test_all_tools
|
|
2.89s call tests/test_install_e2e.py::test_complete_workflow
|
|
...
|
|
```
|
|
|
|
### Optimizing Slow Tests
|
|
|
|
**Strategies:**
|
|
1. **Mock external calls** - Avoid real HTTP requests
|
|
2. **Use smaller test data** - Reduce file sizes
|
|
3. **Parallel execution** - Run tests concurrently
|
|
4. **Mark as slow** - Skip in fast feedback loop
|
|
|
|
```python
|
|
# Mark slow tests
|
|
@pytest.mark.slow
|
|
def test_large_dataset():
|
|
"""Test with large dataset (slow)."""
|
|
pass
|
|
|
|
# Run fast tests only
|
|
pytest tests/ -m "not slow"
|
|
```
|
|
|
|
### Parallel Test Execution
|
|
|
|
```bash
|
|
# Install pytest-xdist
|
|
pip install pytest-xdist
|
|
|
|
# Run tests in parallel (4 workers)
|
|
pytest tests/ -n 4
|
|
|
|
# Auto-detect number of CPUs
|
|
pytest tests/ -n auto
|
|
|
|
# Parallel with coverage
|
|
pytest tests/ -n auto --cov=src/skill_seekers
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging Tests
|
|
|
|
### Running Tests in Debug Mode
|
|
|
|
```bash
|
|
# Show print statements
|
|
pytest tests/test_file.py -v -s
|
|
|
|
# Very verbose output
|
|
pytest tests/test_file.py -vv
|
|
|
|
# Show local variables on failure
|
|
pytest tests/test_file.py -l
|
|
|
|
# Drop into debugger on failure
|
|
pytest tests/test_file.py --pdb
|
|
|
|
# Stop on first failure
|
|
pytest tests/test_file.py -x
|
|
|
|
# Show traceback for failed tests
|
|
pytest tests/test_file.py --tb=short
|
|
```
|
|
|
|
### Using Breakpoints
|
|
|
|
```python
|
|
def test_with_debugging():
|
|
"""Test with debugger breakpoint."""
|
|
result = complex_function()
|
|
|
|
# Set breakpoint
|
|
import pdb; pdb.set_trace()
|
|
|
|
# Or use Python 3.7+ built-in
|
|
breakpoint()
|
|
|
|
assert result == expected
|
|
```
|
|
|
|
### Logging in Tests
|
|
|
|
```python
|
|
import logging
|
|
|
|
def test_with_logging(caplog):
|
|
"""Test with log capture."""
|
|
# Set log level
|
|
caplog.set_level(logging.DEBUG)
|
|
|
|
# Run function that logs
|
|
result = function_that_logs()
|
|
|
|
# Check logs
|
|
assert "Expected log message" in caplog.text
|
|
assert any(record.levelname == "WARNING" for record in caplog.records)
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### 1. Test Naming
|
|
|
|
```python
|
|
# Good: Descriptive test names
|
|
def test_scrape_page_with_missing_title_returns_default():
|
|
"""Test that missing title returns 'Untitled'."""
|
|
pass
|
|
|
|
# Bad: Vague test names
|
|
def test_scraping():
|
|
"""Test scraping."""
|
|
pass
|
|
```
|
|
|
|
### 2. Single Assertion Focus
|
|
|
|
```python
|
|
# Good: Test one thing
|
|
def test_language_detection_python():
|
|
"""Test Python language detection."""
|
|
html = '<code class="language-python">def foo():</code>'
|
|
assert detect_language(html) == 'python'
|
|
|
|
# Acceptable: Multiple related assertions
|
|
def test_config_validation():
|
|
"""Test config has all required fields."""
|
|
assert 'name' in config
|
|
assert 'base_url' in config
|
|
assert 'selectors' in config
|
|
```
|
|
|
|
### 3. Isolate Tests
|
|
|
|
```python
|
|
# Good: Each test is independent
|
|
def test_create_skill(tmp_path):
|
|
"""Test skill creation in isolated directory."""
|
|
skill_dir = tmp_path / 'skill'
|
|
create_skill(skill_dir)
|
|
assert skill_dir.exists()
|
|
|
|
# Bad: Tests depend on order
|
|
def test_step1():
|
|
global shared_state
|
|
shared_state = {}
|
|
|
|
def test_step2(): # Depends on test_step1
|
|
assert shared_state is not None
|
|
```
|
|
|
|
### 4. Keep Tests Fast
|
|
|
|
```python
|
|
# Good: Mock external dependencies
|
|
@patch('requests.get')
|
|
def test_with_mock(mock_get):
|
|
"""Fast test with mocked HTTP."""
|
|
pass
|
|
|
|
# Bad: Real HTTP requests in tests
|
|
def test_with_real_request():
|
|
"""Slow test with real HTTP request."""
|
|
response = requests.get('https://example.com')
|
|
```
|
|
|
|
### 5. Use Descriptive Assertions
|
|
|
|
```python
|
|
# Good: Clear assertion messages
|
|
assert result == expected, f"Expected {expected}, got {result}"
|
|
|
|
# Better: Use pytest's automatic messages
|
|
assert result == expected
|
|
|
|
# Best: Custom assertion functions
|
|
def assert_valid_skill(skill_path):
|
|
"""Assert skill is valid."""
|
|
assert skill_path.exists(), f"Skill not found: {skill_path}"
|
|
assert (skill_path / 'SKILL.md').exists(), "Missing SKILL.md"
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
#### 1. Import Errors
|
|
|
|
**Problem:**
|
|
```
|
|
ImportError: No module named 'skill_seekers'
|
|
```
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Install package in editable mode
|
|
pip install -e ".[all-llms,dev]"
|
|
```
|
|
|
|
#### 2. Fixture Not Found
|
|
|
|
**Problem:**
|
|
```
|
|
fixture 'temp_output_dir' not found
|
|
```
|
|
|
|
**Solution:**
|
|
```python
|
|
# Add fixture to conftest.py or import from another test file
|
|
@pytest.fixture
|
|
def temp_output_dir(tmp_path):
|
|
return tmp_path / 'output'
|
|
```
|
|
|
|
#### 3. Async Test Failures
|
|
|
|
**Problem:**
|
|
```
|
|
RuntimeError: no running event loop
|
|
```
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Install pytest-asyncio
|
|
pip install pytest-asyncio
|
|
|
|
# Mark async tests
|
|
@pytest.mark.asyncio
|
|
async def test_async_function():
|
|
await async_operation()
|
|
```
|
|
|
|
#### 4. Coverage Not Tracking
|
|
|
|
**Problem:**
|
|
Coverage shows 0% or incorrect values.
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Ensure pytest-cov is installed
|
|
pip install pytest-cov
|
|
|
|
# Specify correct source directory
|
|
pytest tests/ --cov=src/skill_seekers
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- **[Code Quality Standards](../reference/CODE_QUALITY.md)** - Linting and quality tools
|
|
- **[Contributing Guide](../../CONTRIBUTING.md)** - Development guidelines
|
|
- **[API Reference](../reference/API_REFERENCE.md)** - Programmatic testing
|
|
- **[CI/CD Configuration](../../.github/workflows/ci.yml)** - Automated testing setup
|
|
|
|
---
|
|
|
|
**Version:** 3.1.0-dev
|
|
**Last Updated:** 2026-02-18
|
|
**Test Count:** 1,880+ tests
|
|
**Coverage:** >85%
|
|
**Status:** ✅ Production Ready
|