SpecificationTest
Docs for xUnit SDK v0.4.0
SpecificationTest is the base class for MSpec-style specification tests. Rules
contain direct assertions — no Given/When/Then steps. Values are embedded in rule
titles and extracted via Rule.Values and Rule.Params.
using SweDevTools.LiveDoc.xUnit;
using Xunit;
using Xunit.Abstractions;
[Specification("Email Validation")]
public class EmailValidationSpec : SpecificationTest
{
public EmailValidationSpec(ITestOutputHelper output) : base(output) { }
[Rule("'user@example.com' is a valid email")]
public void Valid_email()
{
var email = Rule.Values[0].AsString();
Assert.True(EmailValidator.IsValid(email));
}
}
Reference
Constructor
protected SpecificationTest(ITestOutputHelper output)
Every class inheriting SpecificationTest must accept an ITestOutputHelper and pass it to base(output). This produces formatted specification output in Test Explorer.
Parameters
output:ITestOutputHelper— The xUnit output helper injected by the test runner.
[Specification("Calculator")]
public class CalculatorSpec : SpecificationTest
{
public CalculatorSpec(ITestOutputHelper output) : base(output) { }
}
Properties
| Property | Type | Description |
|---|---|---|
Specification | SpecificationContext | Metadata about the current specification (title, description, tags). |
Rule | RuleContext | Metadata and values for the currently executing rule. Provides Values, Params, ValuesRaw, and ParamsRaw. |
Example | dynamic | Dynamic access to the current [Example] row in a [RuleOutline]. |
Rule Property
The Rule property is the primary API for extracting values from rule titles. It mirrors the step-level ctx.Step API used in FeatureTest.
// Quoted values: 'value' syntax
[Rule("Adding '5' and '3' returns '8'")]
public void Addition()
{
var a = Rule.Values[0].AsInt(); // 5
var b = Rule.Values[1].AsInt(); // 3
var expected = Rule.Values[2].AsInt(); // 8
Assert.Equal(expected, a + b);
}
| Sub-property | Type | Description |
|---|---|---|
Rule.Values | LiveDocValueArray | Ordered array of quoted values from the rule title. |
Rule.ValuesRaw | string[] | Raw string values before type conversion. |
Rule.Params | LiveDocValueDictionary | Named parameters from <name:value> syntax. |
Rule.ParamsRaw | IReadOnlyDictionary<string, string> | Raw string parameters before conversion. |
IDisposable
Like FeatureTest, SpecificationTest implements IDisposable and flushes formatted output on disposal:
Specification: Email Validation
Rule: 'user@example.com' is a valid email
✓ passing (2ms)
Usage
Basic: Simple rules
using SweDevTools.LiveDoc.xUnit;
using Xunit;
using Xunit.Abstractions;
[Specification("String Operations", Description = @"
Callers can rely on the string transformations covered by these rules.")]
public class StringSpec : SpecificationTest
{
public StringSpec(ITestOutputHelper output) : base(output) { }
[Rule("Reversing 'hello' returns 'olleh'")]
public void Reverse_string()
{
var (input, expected) = Rule.Values.As<string, string>();
var result = new string(input.Reverse().ToArray());
Assert.Equal(expected, result);
}
[Rule("Uppercasing 'hello' returns 'HELLO'")]
public void Uppercase_string()
{
var (input, expected) = Rule.Values.As<string, string>();
Assert.Equal(expected, input.ToUpper());
}
}
Named parameters with Rule.Params
[Specification("Temperature Conversion")]
public class TempSpec : SpecificationTest
{
public TempSpec(ITestOutputHelper output) : base(output) { }
[Rule("Converting <celsius:100> °C to Fahrenheit gives <fahrenheit:212>")]
public void Celsius_to_fahrenheit()
{
var celsius = Rule.Params["celsius"].AsDouble();
var expected = Rule.Params["fahrenheit"].AsDouble();
var result = celsius * 9.0 / 5.0 + 32;
Assert.Equal(expected, result, precision: 2);
}
}
Tuple deconstruction
Use Rule.Values.As<T1, T2, ...>() for clean multi-value extraction:
[Specification("Arithmetic")]
public class ArithmeticSpec : SpecificationTest
{
public ArithmeticSpec(ITestOutputHelper output) : base(output) { }
[Rule("Adding '5' and '3' returns '8'")]
public void Addition()
{
var (a, b, expected) = Rule.Values.As<int, int, int>();
Assert.Equal(expected, a + b);
}
[Rule("Dividing '10.0' by '3.0' returns '3.33'")]
public void Division_with_precision()
{
var (a, b, expected) = Rule.Values.As<decimal, decimal, decimal>();
Assert.Equal(expected, Math.Round(a / b, 2));
}
}
RuleOutline with Examples
Data-driven rules work the same as ScenarioOutline — method parameters are injected by xUnit:
[Specification("Discount Rules")]
public class DiscountSpec : SpecificationTest
{
public DiscountSpec(ITestOutputHelper output) : base(output) { }
[RuleOutline("A cart with '<itemCount>' items gets '<discount>' percent off")]
[Example(1, 0)]
[Example(5, 10)]
[Example(10, 20)]
public void Volume_discount(int itemCount, int discount)
{
var actual = DiscountEngine.Calculate(itemCount);
Assert.Equal(discount, actual);
}
}
Attach evidence to a rule
When a rule needs supporting evidence, use the inherited Attach, AttachScreenshot,
AttachFile, or AttachJson methods. All four accept an optional title. Evidence
belongs to the rule that created it: a [Rule] exports it in
execution.attachments; each [RuleOutline] example exports its own list in
exampleResults[].result.attachments (identified by testId and rowId).
The outline's aggregate execution does not combine evidence from different rows.
[Rule("The response contains 'approved'")]
public void Approval_response()
{
var expected = Rule.Values[0].AsString();
var response = GetApprovalResponse();
AttachJson(response, "Approval response");
Assert.Contains(expected, response);
}
[RuleOutline("The response for '<input>' is '<expected>'")]
[Example("valid", "approved")]
[Example("invalid", "rejected")]
public void Approval_examples(string input, string expected)
{
var response = GetResponse(input);
AttachJson(response, $"Response for {input}");
Assert.Equal(expected, response);
}
Attach(base64Data, mimeType, title, kind) accepts encoded content and a kind of
"file", "image", or "screenshot". AttachScreenshot(base64Data, title)
uses PNG/screenshot defaults. AttachFile(filePath, title) reads the file and
detects supported image types. Files ending in .mmd or .mermaid use
text/vnd.mermaid and kind "file", so the Viewer can preview a diagram even
when its attachment title omits the extension. AttachJson(data, title)
serializes objects or accepts an existing JSON string. Calls can be repeated
within a rule. Evidence added before an assertion is retained if that
assertion fails; calls after a failing assertion are never reached. Avoid
attaching secrets or personal data.
Feature scenarios continue to associate attachments with their individual steps.
Newtonsoft JSON tokens
Pass a Newtonsoft.Json JObject, JArray, or JSON-compatible JValue
directly to AttachJson—no JsonConvert.SerializeObject call is needed.
Objects, arrays, and string/number/boolean/null values retain their JSON
semantics, including empty containers, Unicode, derived token types, and
tokens nested in CLR properties, arrays, lists, or dictionaries. A string
JValue becomes a quoted JSON string, not unquoted ToString() text.
Newtonsoft.Json is restored automatically as a runtime dependency of the
LiveDoc xUnit package.
For example, this rule validates a receipt's action and records the same receipt as an object, an array, and a nested CLR envelope:
using Newtonsoft.Json.Linq;
using SweDevTools.LiveDoc.xUnit;
using SweDevTools.LiveDoc.xUnit.Core;
using Xunit;
using Xunit.Abstractions;
[Specification("Receipt payloads",
Description = "Receipt JSON provides an action that can be validated and attached as evidence.")]
public sealed class ReceiptSpec(ITestOutputHelper output) : SpecificationTest(output)
{
[Rule("Receipt JSON '{\"action\":\"Finish\",\"receipt\":{\"id\":\"fixture-id\",\"type\":\"Example\"}}' has action 'Finish'")]
public void Receipt_action()
{
var (json, expectedAction) = Rule.Values.As<string, string>();
JObject receipt = JObject.Parse(json);
AttachJson(receipt, "Receipt object");
AttachJson(new JArray(receipt), "Receipt batch");
AttachJson(new { receipt }, "Receipt envelope");
Assert.Equal(expectedAction, receipt.Value<string>("action"));
}
}
The Receipt object attachment contains:
{"action":"Finish","receipt":{"id":"fixture-id","type":"Example"}}
Receipt batch contains an array with that object; Receipt envelope
contains it under a receipt property. All three are UTF-8/base64 attachments
with MIME type application/json and kind file. The action assertion checks
the parsed payload; the attachments are supplementary evidence, not assertions.
Ordinary CLR fields still use System.Text.Json attributes and defaults.
Existing raw JSON strings pass through unchanged.
A standalone JProperty, a JConstructor or JRaw at any depth, and
undefined/comment tokens are not supported. AttachJson throws a
System.Text.Json.JsonException identifying the unsupported token type and
requesting a standard JSON value; it does not record an empty fallback.
Place properties inside a JObject, and parse valid raw JSON into a standard
token before attaching it. Invalid token output also fails JSON validation.
Method name placeholders
When no explicit title is provided, _ALLCAPS segments in the method name become placeholders:
[RuleOutline]
[Example(10, 2, 5)]
[Example(100, 4, 25)]
public void Dividing_A_by_B_returns_RESULT(int a, int b, int result)
{
Assert.Equal(result, a / b);
}
// Output: "Dividing '10' by '2' returns '5'"
Async rules
[Rule("Fetching user '42' returns 'Alice'")]
public async Task Fetch_user_by_id()
{
var (id, expectedName) = Rule.Values.As<int, string>();
var user = await _repository.GetByIdAsync(id);
Assert.Equal(expectedName, user.Name);
}
Specification with description
[Specification("Password Policy", Description = @"
Helps callers identify passwords accepted by the
strength cases covered below.")]
public class PasswordSpec : SpecificationTest
{
public PasswordSpec(ITestOutputHelper output) : base(output) { }
[Rule("'Abc12345!' meets the policy")]
public void Strong_password()
{
var password = Rule.Values[0].AsString();
Assert.True(PasswordPolicy.IsStrong(password));
}
[Rule("'abc' does not meet the policy")]
public void Weak_password()
{
var password = Rule.Values[0].AsString();
Assert.False(PasswordPolicy.IsStrong(password));
}
}
FeatureTest vs. SpecificationTest
| Aspect | FeatureTest | SpecificationTest |
|---|---|---|
| Pattern | BDD / Gherkin | MSpec / Specification |
| Structure | Given / When / Then steps | Direct assertions in rules |
| Best for | Workflows, acceptance tests | Domain policies, units, edge cases, technical contracts |
| Data-driven | [ScenarioOutline] + [Example] | [RuleOutline] + [Example] |
| Value extraction | ctx.Step.Values / ctx.Step.Params | Rule.Values / Rule.Params |
| Reader's focus | Following a workflow | Inspecting exact rules and examples |
Use FeatureTest when a workflow benefits from Given/When/Then. Use
SpecificationTest for focused rules where that narrative adds noise,
including business-owned policies stakeholders may want to inspect.
Group both under the same capability namespace; see
Test Organization.
Formatted Output
Specification: Password Policy
Organizational password strength requirements.
Minimum 8 characters, must include uppercase,
lowercase, digit, and special character.
Rule: 'Abc12345!' meets the policy
✓ passing (1ms)
Rule: 'abc' does not meet the policy
✓ passing (1ms)
See Also
FeatureTest— the BDD/Gherkin-style alternative- Attributes —
[Specification],[Rule],[RuleOutline]details [Example]— data rows for[RuleOutline]- Value Extraction API —
LiveDocValue,As<T>(), type conversions - Context —
RuleContext,SpecificationContext - Your First Spec — step-by-step tutorial