Testing Guide for FCC Extensions¶
FCC enforces a hard 99% line and 80% branch coverage gate in CI
(v1.3.5.3+). Code that ships with FCC is held to that bar, and this
page is about helping your extensions reach it. The patterns below
mirror the tests already in tests/ — browse any of the
test_plugins_*.py files to see them applied.
Test layout¶
Mirror the source layout. If your plugin lives in
src/myproject/fcc_plugin.py, its tests belong in
tests/test_fcc_plugin.py. FCC's own test directory follows the same
rule: every src/fcc/<pkg>/ module has a tests/test_<pkg>_*.py
counterpart. The mirror convention lets coverage reports stay readable
and makes failures easy to localise.
Use pytest, not unittest¶
FCC's test suite is pure pytest. New tests should follow the same
style — plain functions, pytest.fixture for setup, and
pytest.raises for expected errors. The conftest.py files at the
top of tests/ register shared fixtures.
# tests/test_my_vocabulary_provider.py
from __future__ import annotations
import pytest
from fcc.plugins.base import PluginType, VocabularyProviderPlugin
from myproject.fcc_plugin import MyProjectVocabularyProvider
@pytest.fixture
def provider() -> MyProjectVocabularyProvider:
return MyProjectVocabularyProvider()
def test_meta_declares_correct_type(provider):
meta = provider.plugin_meta()
assert meta.plugin_type is PluginType.VOCABULARY_PROVIDERS
assert meta.id == "myproject-vocabulary"
def test_namespace_matches_yaml_prefix(provider):
assert provider.get_namespace() == "myproject"
def test_class_map_covers_expected_keys(provider):
mapping = provider.get_class_map()
assert "myproject:Asset" in mapping
assert "myproject:Event" in mapping
assert all(isinstance(v, type) for v in mapping.values())
def test_graceful_degradation_when_source_missing(monkeypatch, provider):
# Simulate missing source package; class_map must be empty, not raise.
import builtins
real_import = builtins.__import__
def fake_import(name, *args, **kwargs):
if name == "myproject.models":
raise ImportError("simulated")
return real_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", fake_import)
assert provider.get_class_map() == {}
Four cases covers the happy path, the identity invariant, the class-map contract, and the graceful-degradation path — enough to clear 99% line coverage for a typical provider plugin.
Fixtures worth reusing¶
| Fixture | Source | Purpose |
|---|---|---|
persona_registry |
tests/conftest.py |
Full 147-persona registry. |
mock_engine |
tests/conftest.py |
SimulationEngine(mode="mock"). |
event_bus |
tests/conftest.py |
Fresh EventBus, no subscribers. |
tmp_path |
pytest built-in | Per-test temp directory for artifacts. |
If your extension needs a different fixture, define it in your own
conftest.py rather than reaching across test files.
Coverage patterns that save you¶
Test both the happy path and the import-error path for anything
that reads from optional dependencies. FCC's [full] extra is
deliberately wide; half the bugs we ship are in the graceful
degradation path.
Parametrize across all enum values when your code branches on an
enum. PluginType has 11 values; a parametrized test covers all of
them without duplication:
@pytest.mark.parametrize("pt", list(PluginType))
def test_every_plugin_type_is_recognised(pt):
assert pt.value.startswith("fcc.plugins.")
Snapshot-test deterministic traces. The mock simulation engine is
deterministic; its output is suitable for snapshot comparison. Use
pytest.approx only for timings, never for textual step summaries.
Mock AI clients at the provider boundary, not deeper. FCC's
AIProviderPlugin is the single seam between the engine and the
network. Stub one method (build_client) and the rest of the engine
runs unmodified.
Running the suite¶
# Full suite with coverage, matching the CI gate
pytest tests/ --cov=src --cov-branch --cov-fail-under=99
# Fast subset during development
pytest tests/test_plugins_core.py -v
# With ruff lint at the end
make lint test
The --cov-branch flag is what pushes the gate from 99% line to
99% line + 80% branch; it has been on by default since v1.3.1. CI
enforces both.
Integration tests¶
For end-to-end plugin integration, assert that the plugin is
discoverable through importlib.metadata.entry_points. This catches
the most common real-world failure: a plugin that works when imported
directly but is not registered as an entry point.
import importlib.metadata
def test_plugin_is_discoverable():
eps = importlib.metadata.entry_points(
group="fcc.plugins.vocabulary_providers"
)
names = {ep.name for ep in eps}
assert "myproject" in names
Run this test only after pip install -e . — it depends on the
package metadata actually being installed.
What to avoid¶
- Do not touch the network. Every test must pass offline. Use the
mockprovider for AI calls,tmp_pathfor filesystem, and deterministic YAML for data. - Do not skip tests on the coverage-hungry path. A skipped test is worse than no test — it hides a gap that the gate would otherwise surface.
- Do not re-implement
fcc._resources. All packaged data is accessed through that module's helpers. Reimplementing withPath(__file__)chains breaks wheel installs.
Related FCC documentation¶
- Plugin Development — the code under test.
- Integration Patterns — what to test for each of the four integration shapes.
- Quick Start — the minimal end-to-end you can turn into a test.
- For Professionals — Integration Guide — CI wiring that runs the tests above in GitHub Actions / GitLab CI.
- Developer landing — the three developer journeys.
- Notebook
34_developer_first_plugin.ipynb— interactive companion.