Core concepts

The tagging model

Tags are the entire interface between your specification and your test suites. There are exactly three kinds, they do three different jobs, and they never interfere with each other. Understanding this page means understanding SpecTracer.

The three tag roles

@id:

Identity

Goes on a Gherkin scenario. Declares that scenario's stable handle.

@id:FC-42

@scenario:

Reference

Goes on a test result. Claims "I am one of the tests covering that scenario." E2E results get this for free from their own @id: tag — see below.

@scenario:FC-42

@require-*

Expectation

Goes on a Gherkin scenario. Declares which layers must cover it. Never used for matching.

@require-unit

The split matters. Identity and reference tags decide what links to what. Requirement tags decide what counts as done. Because they use different prefixes, adding a requirement can never accidentally create or break a link.

features/login.feature
Feature: User Login

  @id:FC-42 @regression @require-unit:auth @require-integration:auth @require-e2e:auth
  Scenario: Successful login with valid credentials
    Given the user is on the login page
    When they enter valid credentials
    Then they should be redirected to the dashboard

  @id:FC-43
  Scenario: Login with invalid password shows error
    Given the user is on the login page
    When they enter an invalid password
    Then an error message should be displayed

FC-43 declares no requirements, so it falls back to a bare @require-e2e — see defaults.

How linking works

The linker is deliberately the dumbest part of the tool. For each scenario it collects the set of values after @id:, then keeps every test result carrying an @scenario: tag whose value is in that set. E2E results get one extra rule: an @id: tag on an E2E result counts the same as a matching @scenario: tag. This is not a new tagging convention — it's recognizing that an E2E result comes from running the same feature file the scenario is declared in, so its Cucumber JSON already carries the scenario's own @id: tag verbatim. You don't have to add a redundant @scenario: tag next to it just to make linking happen. Unit and integration results aren't run from the feature file, so they still need an explicit @scenario: tag.

spec_tracer/linker.py — abridged
def link(scenarios, results):
    links = {}
    for scenario in scenarios:
        ids = {t[4:] for t in scenario.tags if t.startswith("@id:")}
        linked = [
            r for r in results
            if any(t.startswith("@scenario:") and t[10:] in ids for t in r.tags)
            or (r.layer == "e2e" and any(t.startswith("@id:") and t[4:] in ids for t in r.tags))
        ]
        links[id(scenario)] = linked
    return links

The rules that follow from that

RuleMeaning
Prefix-filtered Only @scenario: on results, @id: on scenarios, and (E2E results only) @id: on results participate. @regression, @smoke, and everything else is ignored during linking.
Exact value match @scenario:FC-42 matches @id:FC-42 only — not @id:FC-4, not @id:FC-42-smoke. No prefix matching, no globbing, no case folding.
OR within a result A result tagged @scenario:FC-42 and @scenario:FC-43 links to any scenario carrying either id. One test can legitimately cover several scenarios.
Collisions link everywhere If two scenarios share an @id: value — in the same file or different files — a single matching result links to both. Usually a mistake; keep ids unique.
Scenario-level tags only Tags on the Feature: line are not inherited by its scenarios. This is a common surprise.
@require-* is inert Requirement tags are excluded from linking entirely, so they can never collide with identity tags.
No tag expressions Matching is exact string equality. There is no and / or / not boolean tag logic, and no wildcards. If you need selective execution, do it in your test runner's own tag filtering before the results reach SpecTracer.

Where tags are read from

Different frameworks put metadata in different places, so SpecTracer checks all the plausible ones rather than mandating a single convention.

SourceLocations checked
JUnit XML
(unit & integration)
The name attribute of <testcase>
The classname attribute of <testcase>
<properties><property> elements inside <testcase>
Cucumber JSON
(any layer)
The native scenario-level tags array

For JUnit, the <property> route is the cleanest — it keeps tags out of your test names. But embedding the tag in the test name works fine and needs no plugin, which is why SpecTracer's own suite does it:

a real test name from SpecTracer's own report
test_render_dashboard_sections_present[@scenario:FC-006]
Unit and integration layers can be BDD-based too

Cucumber JSON isn't only for E2E. A unit- or integration-level Gherkin scenario tagged @scenario:FC-42 links back to the E2E-defined spec scenario exactly like a JUnit test with the same tag would — as long as the scenario itself is made to pass. unit and integration config entries accept JUnit XML and Cucumber JSON interchangeably; format is auto-detected per file, so a module can mix both.

Only @scenario: matters on these results. @id: has no linking effect on unit or integration results — SpecTracer prints a warning if one carries it, since it's almost always a copy-paste leftover from an E2E-style feature file rather than a deliberate identity tag. (On E2E results, @id: does link — see How linking works.)

Layer requirements

A link tells you a test exists. A requirement tells you whether that's enough. Three requirement tags exist, matching the three layers:

Each declared requirement that has no linked result is flagged as missing on the scenario row in the report, and drags down both headline numbers. The set of all declared requirements across all scenarios is the denominator of the declared tests matched percentage.

Requirements are a promise, not a filter Declaring @require-unit doesn't stop an E2E test from linking to the scenario — links are independent of requirements. It only means the scenario isn't considered fully covered until a unit-layer result shows up too.

Module scope

On a large codebase "there is a unit test for this" is often too weak. You want "there is a unit test in the billing module for this". All three requirement tags accept an optional :modulename suffix for exactly that.

The module names come from the keys of the layer objects in your config:

spectracer.config.json
{
  "unit": {
    "":        ["./reports/unit.xml"],          // unscoped
    "auth":    ["./reports/auth-unit.xml"],     // module "auth"
    "billing": ["./reports/billing-unit.xml"]
  },
  "e2e": {
    "":         ["./reports/e2e.json"],
    "checkout": ["./reports/checkout-e2e.json"]
  }
}

Every result parsed out of a file is stamped with the module key it was registered under. Then:

RequirementSatisfied byNot satisfied by
@require-unit
(bare)
Any linked unit result, whatever module it came from — including unscoped. A linked result from a different layer.
@require-unit:auth Only a linked unit result registered under exactly the "auth" key. An unscoped result (key ""), or one under "billing". Module matching is strict.
The strictness catches people out If you tag @require-unit:auth but leave all your unit XML registered under "", the requirement will read as missing forever. Either split the files by module in the config, or drop the :auth suffix.

Module scope works identically for all three layers, which makes it a good fit for microservice-per-team repos: tag scenarios @require-unit:my-service and only that team's results count.

Defaults & edge cases

SituationBehaviour
Scenario has no @require-* tag at allDefaults to a bare, unscoped @require-e2e.
Scenario has no @id: tagNothing can ever link to it. It appears in the report as permanently incomplete.
Test result matches no scenarioListed under Unlinked Tests. It still counts toward the pyramid and pass-rate stats.
Scenario matches no testShown as incomplete, with each declared layer flagged missing.
Tags on the Feature: lineIgnored. Not inherited by scenarios.
Scenario Outline: / Examples:Parsed as a single scenario, named from the Scenario Outline: line. Individual Examples rows are not expanded.
Rule:, Background:, non-English dialectsLeft to whatever your Gherkin or E2E framework does with them. SpecTracer only understands Feature:, tags, Scenario: / Scenario Outline:, and steps.
Unicode and special charactersPreserved, and HTML-escaped in the report.

Naming conventions that hold up

None of this is enforced by the tool — it's what tends to survive contact with a real team.

PatternExampleUse
@id:<VALUE>@id:FC-42Scenario identity, on Gherkin scenarios
@scenario:<VALUE>@scenario:FC-42Test reference, on test results
@require-<layer>@require-unitRequired layer coverage
@require-<layer>:<module>@require-e2e:checkoutRequired layer, scoped to a config module key
@<classification>@regression, @smokeYour own tags — ignored by SpecTracer