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
Identity
Goes on a Gherkin scenario. Declares that scenario's stable handle.
@id:FC-42
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
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.
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.
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
| Rule | Meaning |
|---|---|
| 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. |
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.
| Source | Locations 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:
test_render_dashboard_sections_present[@scenario:FC-006]
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:
- @require-unit — satisfied by a linked result collected under
unit - @require-integration — satisfied by a linked result collected under
integration - @require-e2e — satisfied by a linked result collected under
e2e
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.
@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:
{
"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:
| Requirement | Satisfied by | Not 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. |
@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
| Situation | Behaviour |
|---|---|
Scenario has no @require-* tag at all | Defaults to a bare, unscoped @require-e2e. |
Scenario has no @id: tag | Nothing can ever link to it. It appears in the report as permanently incomplete. |
| Test result matches no scenario | Listed under Unlinked Tests. It still counts toward the pyramid and pass-rate stats. |
| Scenario matches no test | Shown as incomplete, with each declared layer flagged missing. |
Tags on the Feature: line | Ignored. 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 dialects | Left to whatever your Gherkin or E2E framework does with them. SpecTracer only understands Feature:, tags, Scenario: / Scenario Outline:, and steps. |
| Unicode and special characters | Preserved, 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.
| Pattern | Example | Use |
|---|---|---|
@id:<VALUE> | @id:FC-42 | Scenario identity, on Gherkin scenarios |
@scenario:<VALUE> | @scenario:FC-42 | Test reference, on test results |
@require-<layer> | @require-unit | Required layer coverage |
@require-<layer>:<module> | @require-e2e:checkout | Required layer, scoped to a config module key |
@<classification> | @regression, @smoke | Your own tags — ignored by SpecTracer |
- Make ids opaque and stable.
FC-42or a ticket key beatslogin-happy-path, because a renamed scenario shouldn't break every test that references it. - Never reuse an id. Collisions link one result to several scenarios and quietly inflate coverage.
- Agree the module vocabulary once. Module keys live in two places — the config
and the
@require-*:suffixes — and they must match exactly. - Review tags like code. A missing
@scenario:tag looks identical to a missing test in the report, which is the point.