Architecture Overview
ProTest follows a Ports & Adapters (hexagonal) architecture, separating the domain logic from external interfaces.
Layers
┌─────────────────────────────────────────────────────────┐
│ Adapters │
│ ┌─────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ │
│ │ CLI │ │ Loader │ │ Reporters│ │ Plugins │ │
│ └────┬────┘ └────┬───┘ └────┬─────┘ └────┬─────┘ │
└───────┼────────────┼───────────┼─────────────┼─────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Ports (API) │
│ run_session() collect_tests() list_tags() │
│ load_session() │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Domain │
│ ProTestSession TestRunner Collector FixtureContainer │
│ ProTestSuite EventBus │
└─────────────────────────────────────────────────────────┘
Domain Layer
The core test execution logic, independent of how tests are discovered or results are displayed.
| Component | Location | Responsibility |
|---|---|---|
ProTestSession |
core/session.py |
Test session container |
ProTestSuite |
core/suite.py |
Test grouping |
TestRunner |
core/runner.py |
Test execution |
Collector |
core/collector.py |
Test discovery |
FixtureContainer |
di/container.py |
Fixture resolution |
EventBus |
events/bus.py |
Event dispatching |
Ports Layer (protest/api.py)
The public API for programmatic use. These functions don't depend on any specific adapter.
from protest import ProTestSession, run_session, collect_tests, list_tags
session = ProTestSession()
# ... define tests ...
# Run tests
success = run_session(session, concurrency=4, exitfirst=True)
# Collect without running
items = collect_tests(session, include_tags={"unit"})
# List declared tags
tags = list_tags(session)
Loader (protest/loader.py)
Loads sessions from module paths:
from protest import load_session, LoadError
try:
session = load_session("mymodule:session", app_dir="src")
except LoadError as e:
print(f"Failed to load: {e}")
Adapters Layer
CLI (protest/cli/)
Entry point for command-line usage. Parses arguments and calls the API.
The CLI is a thin wrapper that:
1. Parses arguments (argparse)
2. Uses load_session() to get the session
3. Calls run_session() with options
4. Returns exit code (0/1)
Reporters
Plugins that subscribe to events and format output:
- RichReporter - Rich terminal output
- AsciiReporter - Plain text fallback
- CTRFReporter - JSON for CI/CD
Plugins
Custom extensions that hook into the event bus:
- CachePlugin - Last-failed mode
- TagFilterPlugin - Tag filtering
- SuiteFilterPlugin - Suite filtering
- KeywordFilterPlugin - Keyword filtering
Module Structure
protest/
├── core/ # Domain: Session, Suite, Runner, Collector
├── di/ # Domain: FixtureContainer, Markers (Use), Validation
├── entities/ # Domain: Dataclasses (Fixture, TestItem, TestResult)
├── events/ # Domain: Event bus
├── execution/ # Domain: AsyncBridge, Capture, Context
├── fixtures/ # Domain: Built-in fixtures (caplog, mocker)
│
├── api.py # Ports: Public API
├── loader.py # Ports: Module loading
│
├── cli/ # Adapter: Command-line interface
├── reporting/ # Adapter: Reporters
├── cache/ # Adapter: CachePlugin
├── tags/ # Adapter: TagFilterPlugin
└── filters/ # Adapter: Suite/Keyword filters
See Also
- Event Bus - Event dispatching internals
- Dependency Injection - Fixture resolution
- Plugins - Writing plugins