- Setup
- Running Tests
- Test Structure
- Test Modules
- Fixtures
- Async Testing
- Mocking HTTP Requests
- Writing New Tests
- Linting
Install the project with dev dependencies:
pip install -e ".[dev]"This pulls in:
pytest-- test runnerpytest-asyncio-- async test supportaioresponses-- mock aiohttp requestspytest-aiohttp-- aiohttp test utilitiesruff-- linter
# Run the full test suite
pytest
# Run with verbose output
pytest -v
# Run a specific test file
pytest tests/test_queue.py
# Run a specific test function
pytest tests/test_queue.py::test_scope_dw_only
# Run tests matching a keyword
pytest -k "parser"
# Run with stdout visible (useful for debugging)
pytest -stests/
├── conftest.py # Shared fixtures (sample HTML, seed files, temp dirs)
├── test_config.py # Configuration loading and merging
├── test_models.py # CrawlResult serialization
├── test_queue.py # URLQueue: scope, dedup, depth, patterns, modes
├── test_parser.py # HTML parsing: titles, descriptions, links
├── test_fetcher.py # HTTP fetching: success, errors, timeouts
├── test_crawler.py # CrawlEngine: basic crawl, depth limits, unique_domains
├── test_engine_pause.py # Engine pause/resume functionality
├── test_exporters.py # JSON, JSONL, CSV, SQLite exporters
├── test_utils.py # URL normalization, domain extraction, scope matching
├── test_session.py # Session save/load/restore
├── test_stats.py # Statistics tracking
├── test_auth.py # Password hashing, session tokens, tampering
├── test_manager.py # CrawlManager: job creation, removal, graph data
├── test_webadmin_api.py # Web Admin REST API endpoints
├── test_webui.py # Web UI server
├── test_cli.py # CLI command tests
├── test_scaffold.py # Project structure validation
└── test_integration.py # End-to-end crawl workflows
Tests CrawlConfig -- loading from TOML, merging CLI overrides, default values.
Tests CrawlResult.to_dict() and CrawlResult.from_dict() round-trip serialization.
Tests URLQueue behavior:
- Scope filtering (deepweb-only, clearweb-only)
- URL deduplication
- Depth tracking and max_depth enforcement
- Include/exclude regex patterns
unique_domainsmode- Shared deduplication across queues
- Snapshot and restore
Tests HTML parsing:
- Title extraction
- Meta description extraction with
<p>fallback - Link discovery and normalization
- Edge cases (empty HTML, no links, malformed markup)
Tests async HTTP fetching using aioresponses:
- Successful fetch and result construction
- HTTP error handling
- Timeout handling
- Non-HTML content type rejection
Tests CrawlEngine end-to-end with mocked HTTP:
- Basic crawl with link following
- Depth limiting
unique_domainsmode
Tests the pause/resume mechanism of CrawlEngine.
Tests all four export formats:
- JSON array output
- JSONL line-by-line output
- CSV with correct headers and rows
- SQLite round-trip (write then query)
Tests session persistence:
- Save queue state and config to JSON
- Load and restore from saved session
- Session file auto-discovery
Tests the Web Admin authentication system:
- Password hashing and verification
- Session token creation and validation
- Tampered token rejection
- Token expiry
Tests CrawlManager:
- Job creation and listing
- Job removal
- Domain graph data generation
Tests Web Admin REST endpoints:
- Job, campaign, and result API operations
- Authentication enforcement
Full crawl workflows from seed to exported output.
Defined in tests/conftest.py:
A complete HTML document with title, meta description, and links (both absolute and relative). Used by parser and crawler tests.
HTML without a <meta name="description"> tag, triggering the <p> fallback path.
Creates a temporary seed file containing both .onion and clearweb URLs plus a comment line. Returns the Path.
Creates and returns a temporary output directory.
CrawlKit uses pytest-asyncio with asyncio_mode = "auto" (configured in pyproject.toml). This means:
- Test functions declared as
async defare automatically treated as async tests - No need for
@pytest.mark.asynciodecorators - Fixtures can also be async
Example:
async def test_fetch_success(aioresponses_mock):
aioresponses_mock.get("http://example.com", body="<html>...</html>")
result, html = await fetch_page("http://example.com", timeout=10, ssl_ctx=None)
assert result.status_code == 200Use aioresponses to mock aiohttp calls without hitting the network:
from aioresponses import aioresponses
async def test_my_feature():
with aioresponses() as m:
m.get("http://example.com", body="<html><title>Test</title></html>")
# Call code that uses aiohttp to fetch the URLFor tests that need an aiohttp server instance (Web Admin API tests), use pytest-aiohttp fixtures.
- File naming:
tests/test_<module>.py - Function naming:
test_<behavior_being_tested> - Use fixtures: leverage
tmp_path,seed_file,output_dirfrom conftest - Async tests: just declare as
async def-- auto mode handles the rest - Mock external I/O: never make real HTTP requests in tests
# tests/test_utils.py
from crawlkit.utils import get_main_domain
def test_get_main_domain_standard():
assert get_main_domain("https://www.example.com/page") == "example.com"
def test_get_main_domain_onion():
assert get_main_domain("http://abc123.onion/path") == "abc123.onion"# tests/test_exporters.py
from crawlkit.models import CrawlResult
async def test_my_exporter_writes(tmp_path):
from crawlkit.exporters.my_exporter import MyExporter
exporter = MyExporter(tmp_path / "output.myext")
result = CrawlResult(
url="http://example.com",
title="Test",
description="Desc",
timestamp=1700000000.0,
status_code=200,
content_length=100,
depth=0,
)
await exporter.write(result)
await exporter.close()
# Assert the output file contains the expected data# Check for issues
ruff check crawlkit/ tests/
# Auto-fix where possible
ruff check --fix crawlkit/ tests/Ruff is configured in pyproject.toml with:
- Target: Python 3.13
- Line length: 120 characters