Start here

Getting started

By the end of this page you'll have a real SpecTracer report open in your browser, built from one tagged scenario and the test output your project already produces. Budget about ten minutes.

What SpecTracer is not It doesn't run your tests and it never reads your source code. It reads .feature files, reads test-result files your runners emit, and matches them to each other. Everything below assumes you already have tests that run.

Prerequisites

1 · Install

pip
pip install spec-tracer
uv
uv pip install spec-tracer

Either way you get a global spec-tracer command. Confirm it resolves:

terminal
spec-tracer
# Errors out because there's no config yet — that's the expected
# result at this point, and it means the CLI is installed.

From source

If you want to hack on SpecTracer itself:

terminal
git clone https://github.com/ampyard/spec-tracer.git
cd spec-tracer
uv sync

# run the tool from the checkout
uv run spec-tracer

# build a wheel for local testing
uv build   # → dist/spec_tracer-*.whl

# run SpecTracer's own suites and regenerate its self-report
uv sync --group dev
uv run python run_local.py

2 · Tag a scenario

Pick one scenario in one .feature file. Give it a stable identity with @id: and declare which layers you expect to cover it with @require-*.

features/checkout.feature
Feature: Checkout

  @id:FC-42 @require-unit @require-e2e @regression
  Scenario: Card payment succeeds
    Given a cart with one item
    When the customer pays by card
    Then the order is confirmed

Three different jobs are being done by those tags:

TagRole
@id:FC-42Identity. The handle tests use to point back at this scenario. Any stable string works — FC-42, CHECKOUT-1, a Jira key.
@require-unit @require-e2eExpectation. "This scenario is not properly covered until both a unit test and an E2E test link to it." Never used for matching.
@regressionIgnored. Your own classification tags pass through untouched and never interfere.
If you omit @require-* entirely The scenario defaults to a bare @require-e2e. That's a deliberate default — but it means an untagged scenario will read as "missing E2E coverage" rather than "no expectations set".

3 · Tag the tests

Now point the tests back. A test claims a scenario with @scenario:FC-42. Matching is exact string equality on the value after the prefix — @scenario:FC-42 matches @id:FC-42 and nothing else.

SpecTracer reads tags from wherever your framework happens to put them, so you have options:

pytest — in the test name
def test_card_payment_succeeds():  # rename to embed the tag:
    ...

def test_card_payment_succeeds_at_scenario_FC_42():
    ...

# …or parametrise the id so it lands in the JUnit `name`:
@pytest.mark.parametrize("tag", ["@scenario:FC-42"])
def test_card_payment_succeeds(tag):
    ...
JUnit XML — as a property (the cleanest option)
<testcase name="test_card_payment_succeeds" classname="tests.unit.test_payment">
  <properties>
    <property name="scenario" value="@scenario:FC-42"/>
  </properties>
</testcase>
Cucumber / behave — a native scenario tag
  @scenario:FC-42
  Scenario: Card payment succeeds end to end
    Given ...
E2E results don't need this step

The @scenario: tag above is for a separate unit- or integration-level BDD scenario claiming FC-42. If instead you're running the very feature file that declares @id:FC-42 as your E2E suite, its Cucumber JSON output already carries that @id: tag — SpecTracer links E2E results by @id: automatically, so there's nothing to add. See the tagging model for why.

Where SpecTracer looks

JUnit XML — the name attribute, the classname attribute, and <properties><property> elements.

Cucumber JSON — the scenario-level tags array.

Whichever of those your framework populates, the tag will be found. Pick the one that's least awkward for your team.

4 · Emit the result files

Run your suites as you normally would, adding the flag that writes machine-readable output. Nothing about how your tests execute changes.

terminal
# unit + integration → JUnit XML
pytest tests/unit        --junitxml=reports/unit.xml
pytest tests/integration --junitxml=reports/int.xml

# E2E → Cucumber JSON
behave features/ --format json -o reports/e2e.json

Other stacks, same idea:

RunnerFlagFormat
pytest--junitxml=out.xmlJUnit XML
Maven Surefire / Gradleon by defaultJUnit XML in target/surefire-reports / build/test-results
Jestjest-junit reporterJUnit XML
Vitest--reporter=junit --outputFile=out.xmlJUnit XML
Gogo test -v ./... | go-junit-reportJUnit XML
Cucumber-JVM / Cucumber.js--format json:out.jsonCucumber JSON
Cypress / Playwright with CucumberCucumber JSON formatterCucumber JSON

5 · Write the config

SpecTracer has no CLI flags at all — everything lives in one JSON file. Create spectracer.config.json in your project root:

spectracer.config.json
{
  "features": ["./features"],
  "unit":        { "": ["./reports/unit.xml"] },
  "integration": { "": ["./reports/int.xml"] },
  "e2e":         { "": ["./reports/e2e.json"] },
  "output":      "./reports/spectracer-report.html"
}

Two things about that shape usually surprise people the first time:

Only features and output are mandatory. Full key-by-key reference: Configuration.

6 · Run it

terminal
spec-tracer

That's the whole command. It auto-discovers spectracer.config.json in the current directory. To use a differently named or located config, pass it as the one and only argument:

terminal
spec-tracer config/coverage.json
Shell note Use forward slashes (./spectracer.config.json) or a bare filename. A leading .\ in PowerShell style gets mangled by POSIX shells such as Git Bash and WSL, where backslash is the escape character.

Open reports/spectracer-report.html in any browser. It's a single self-contained file — no server, no build step, safe to email or archive.

Read the report

The first thing to look at is the two headline numbers on the Overview page.

SpecTracer Overview page showing declared tests matched, scenarios fully matched, tests passed, and four health-check cards.
Overview — headline coverage plus the four health signals.
NumberWhat it counts
Declared tests matched Of all the @require-* layer/module pairs you declared across every scenario, how many actually have a linked result. This is the headline coverage percentage.
Scenarios fully matched How many scenarios have every layer they declared satisfied. Strictly harder than the number above — one missing layer disqualifies the whole scenario.
Tests passed Straightforward pass rate across all collected results, linked or not.

If your first run shows 0%, that's almost always a tag mismatch rather than a broken setup. Check the Unlinked Tests page — results with tags that matched nothing are listed there, which usually makes the typo obvious. See Troubleshooting for the full checklist.

Grow from here

Rolling this out across a real codebase works best in this order:

1

One layer, then more

Start with whichever layer is easiest to tag. Add @require-unit and @require-integration expectations as those suites get tagged, not before — otherwise your first report is a wall of red and nobody trusts it.

2

Commit the config

spectracer.config.json belongs in version control — it's your team's coverage contract. Add the generated report to .gitignore; it's a build artifact.

3

Put it in CI

Generate the report on every run and upload it as an artifact. See CI/CD recipes for GitHub Actions, GitLab CI, and Jenkins.

4

Then make it a gate

Once the numbers are honest, turn on error_on_failure and add health checks to fail_on. Enforcing a standard you haven't met yet just teaches people to skip the check.