Skip to main content

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​

PropertyTypeDescription
SpecificationSpecificationContextMetadata about the current specification (title, description, tags).
RuleRuleContextMetadata and values for the currently executing rule. Provides Values, Params, ValuesRaw, and ParamsRaw.
ExampledynamicDynamic 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-propertyTypeDescription
Rule.ValuesLiveDocValueArrayOrdered array of quoted values from the rule title.
Rule.ValuesRawstring[]Raw string values before type conversion.
Rule.ParamsLiveDocValueDictionaryNamed parameters from <name:value> syntax.
Rule.ParamsRawIReadOnlyDictionary<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.

Nonstandard tokens

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​

AspectFeatureTestSpecificationTest
PatternBDD / GherkinMSpec / Specification
StructureGiven / When / Then stepsDirect assertions in rules
Best forWorkflows, acceptance testsDomain policies, units, edge cases, technical contracts
Data-driven[ScenarioOutline] + [Example][RuleOutline] + [Example]
Value extractionctx.Step.Values / ctx.Step.ParamsRule.Values / Rule.Params
Reader's focusFollowing a workflowInspecting exact rules and examples
When to choose which

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​