Prompt file imported from dingjimmy/Stub-Gateway (
.github/prompts/create-acceptance-tests.prompt.md). Copyright stays with the author.
Create Acceptance Tests
You are helping the user produce a scaffolded acceptance test project from specs/*.spec files — the third artefact in the Agent-First / Agent-Enhanced development workflow.
Step 1 produced docs/requirements.md (via /create-requirements).
Step 2 produced specs/*.spec (via /create-spec).
This step produces a runnable-but-skipped test project that gives the team a traceable, executable skeleton derived directly from the specs.
What this skill produces
- A
.NET 10xUnit test project attests/<ProjectName>.AcceptanceTests/ - One test file per spec file, named
<FeatureName>Tests.cs - One test class per
Rule:block, named after the rule - One
[Example]test method perExample:block, named after the example - All tests start as
[Example("...", skip: "Not yet implemented")] - Given/When/Then steps from the spec included as comments inside each test body
- A solution file at the repository root (if one does not already exist)
- Custom attributes (
Feature,Rule,Example,Examples) inAttributes.cs
Conventions
Namespace structure
Use namespaces to express the Feature → Rule → Example hierarchy:
- Feature → namespace e.g.
OrderTracker.AcceptanceTests.ProgressOrders - Rule → class within that namespace e.g.
public class OnlyForwardTransitionsBetweenAdjacentStatusesAreAllowed - Example → test method decorated with
[Example("...", skip: "...")]
Do not use nested classes. One namespace per spec file; all rule classes live flat within it.
Test method naming
Method names are PascalCase versions of the example title (spaces removed, punctuation dropped). The human-readable title is carried by the [Example] attribute's displayName parameter — this is what appears in test output and tooling.
GWT comments
Inside each test method body, reproduce the full Given/When/Then block from the spec as line comments, preserving And lines and data tables:
[Example("Skipping a status is rejected", skip: "Not yet implemented")]
public Task SkippingAStatusIsRejected()
{
// Given an order in status "received"
// When the operator tries to mark it as "shipped"
// Then the change is rejected
// And the operator sees a message that the order must be picked and packed first
// And the order's status is still "received"
return Task.CompletedTask;
}
Custom attributes
Create Attributes.cs in the test project root. This file defines:
| Attribute | Base | Purpose |
|---|---|---|
[Feature("...")] |
ITraitAttribute |
Applied to the class — sets the Feature trait |
[Rule("...")] |
ITraitAttribute |
Applied to the class — sets the Rule trait |
[Example("displayName", skip: "...")] |
FactAttribute |
Applied to the method — sets display name and optional skip reason |
[Examples("displayName")] |
TheoryAttribute |
Applied to data-driven methods — sets display name |
Feature and Rule are implemented via ITraitAttribute and a custom ITraitDiscoverer so they appear as filterable traits in Visual Studio Test Explorer. TraitAttribute is sealed and cannot be subclassed directly.
using Xunit.Abstractions;
using Xunit.Sdk;
namespace <ProjectName>.AcceptanceTests;
[TraitDiscoverer("<ProjectName>.AcceptanceTests.FeatureDiscoverer", "<ProjectName>.AcceptanceTests")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public sealed class FeatureAttribute(string name) : Attribute, ITraitAttribute
{
public string Name { get; } = name;
}
[TraitDiscoverer("<ProjectName>.AcceptanceTests.RuleDiscoverer", "<ProjectName>.AcceptanceTests")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public sealed class RuleAttribute(string name) : Attribute, ITraitAttribute
{
public string Name { get; } = name;
}
public class FeatureDiscoverer : ITraitDiscoverer
{
public IEnumerable<KeyValuePair<string, string>> GetTraits(IAttributeInfo traitAttribute)
{
yield return new KeyValuePair<string, string>("Feature", traitAttribute.GetNamedArgument<string>("Name"));
}
}
public class RuleDiscoverer : ITraitDiscoverer
{
public IEnumerable<KeyValuePair<string, string>> GetTraits(IAttributeInfo traitAttribute)
{
yield return new KeyValuePair<string, string>("Rule", traitAttribute.GetNamedArgument<string>("Name"));
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class ExampleAttribute : FactAttribute
{
public ExampleAttribute(string displayName, string? skip = null)
{
DisplayName = displayName;
Skip = skip;
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class ExamplesAttribute : TheoryAttribute
{
public ExamplesAttribute(string displayName) => DisplayName = displayName;
}
Test file shape
Each test file maps to one spec file. The file begins with a comment header and a namespace declaration, then the rule classes follow flat. Using the same example as above:
// Feature: Move orders through fulfilment statuses
// Spec: specs/progress-orders.spec
namespace OrderTracker.AcceptanceTests.ProgressOrders;
[Feature("Move orders through fulfilment statuses")]
[Rule("Only forward transitions between adjacent statuses are allowed")]
public class OnlyForwardTransitionsBetweenAdjacentStatusesAreAllowed
{
[Example("Skipping a status is rejected", skip: "Not yet implemented")]
public Task SkippingAStatusIsRejected()
{
// Given an order in status "received"
// When the operator tries to mark it as "shipped"
// Then the change is rejected
// And the operator sees a message that the order must be picked and packed first
// And the order's status is still "received"
return Task.CompletedTask;
}
}
Each rule class carries both [Feature] and [Rule] attributes so every test is self-describing when viewed in isolation.
Process
- Read all spec files in
specs/to understand the full set of features, rules, and examples. - Check for an existing test project in
tests/. If one exists, read it and amend rather than regenerate. - Scaffold the project using
dotnet new xunit -f net10.0. AddMicrosoft.Playwrightas a package reference. - Add the project to the solution using
dotnet sln add. Create a solution file first if none exists. - Delete the template test file (
UnitTest1.cs) created bydotnet new. - Create
Attributes.cswith the custom attribute definitions. - Create one test file per spec file, following the namespace/class/method conventions above.
- Run
dotnet testand confirm: build succeeds, 0 failed, all tests skipped. - Report back with a summary: files created, test count per feature, and a reminder that tests are ready to be implemented against the running artifact.
Amendment mode
If a test project already exists, apply the minimum change:
- Add test files for any spec files that have no corresponding test file.
- Add rule classes and example methods for any rules/examples not yet represented.
- Do not regenerate or overwrite existing test files unless the user explicitly asks.
- Preserve existing method names, namespace structure, and attribute usage.
Definition of "acceptance test"
Acceptance tests in this workflow verify the observable behaviour of a deployable artifact (executable binary, Docker container, hosted service). They are:
- Black-box: they interact only through the artifact's public interfaces (HTTP, UI, CLI).
- Technology-agnostic in structure: the test structure derives from the spec, not from internal implementation details.
- Not unit tests: they do not instantiate classes, mock dependencies, or test internal methods.
The scaffolded methods are placeholders. When implemented, each method should start the artifact (or connect to a running instance) and exercise it through its public interface, asserting only what the Given/When/Then steps describe.
Self-check before finishing
- One test file per spec file; filenames match the spec slug.
- Every
Rule:in every spec has a corresponding class in the test file. - Every
Example:in every spec has a corresponding[Example(..., skip: "Not yet implemented")]method. - All GWT steps are reproduced as comments inside the method body.
-
[Feature]and[Rule]attributes are applied to every rule class. -
Attributes.cscompiles cleanly (no subclassing of sealedTraitAttribute). -
dotnet testreports 0 failed, all skipped.