Skip to main content

FeatureTest

Docs for xUnit SDK v0.4.0

FeatureTest is the base class for all BDD/Gherkin-style tests. It provides the Given(), When(), Then(), And(), and But() step methods, manages the test execution context, and produces beautifully formatted Gherkin output.

using SweDevTools.LiveDoc.xUnit;
using Xunit;
using Xunit.Abstractions;

[Feature("Shopping Cart")]
public class ShoppingCartTests : FeatureTest
{
public ShoppingCartTests(ITestOutputHelper output) : base(output) { }

[Scenario]
public void Free_shipping_for_large_orders()
{
Given("an order totalling '150.00' dollars", ctx =>
{
_total = ctx.Step!.Values[0].AsDecimal();
});

Then("shipping is 'Free'", ctx =>
{
Assert.Equal(ctx.Step!.Values[0].AsString(), CalculateShipping(_total));
});
}
}

Reference​

Constructor​

protected FeatureTest(ITestOutputHelper output)

Every class inheriting FeatureTest must accept an ITestOutputHelper and pass it to base(output). This is xUnit's standard mechanism for capturing test output — the formatted Gherkin results appear in Test Explorer's detail pane.

Parameters​

  • output: ITestOutputHelper — The xUnit output helper injected by the test runner. Required for formatted step output.
[Feature("User Authentication")]
public class AuthTests : FeatureTest
{
public AuthTests(ITestOutputHelper output) : base(output) { }
}

Properties​

PropertyTypeDescription
FeatureFeatureContextMetadata about the current feature (title, description, tags).
ScenarioScenarioContextMetadata about the currently executing scenario.
ExampledynamicDynamic access to the current [Example] row in a [ScenarioOutline].

Step Methods​

FeatureTest provides five step methods: Given(), When(), Then(), And(), and But(). Each has four overloads:

// Sync without context
void Given(string title, Action action);

// Sync with context (for value extraction)
void Given(string title, Action<LiveDocContext> action);

// Async without context
Task Given(string title, Func<Task> action);

// Async with context
Task Given(string title, Func<LiveDocContext, Task> action);

All five keywords (Given, When, Then, And, But) share these same overloads. See the Step Methods reference for full details.

JSON evidence​

Call the inherited AttachJson(data, title) inside a step to attach JSON evidence to that step. Newtonsoft.Json JObject, JArray, and JSON-compatible JValue values can be passed directly, including tokens nested in CLR objects or collections; pre-serialization is unnecessary. The same serialization contract applies to Specification rules. See Newtonsoft JSON tokens for a typed example, automatic runtime dependency, and unsupported-token diagnostics. Ordinary CLR serialization and raw JSON string pass-through remain unchanged.

EnsureContext()​

protected LiveDocContext EnsureContext()

Returns the current LiveDocContext, initializing it if needed. This is primarily used internally by the step methods but can be called directly if you need context access outside of a step.

[Scenario]
public void Access_feature_metadata()
{
var ctx = EnsureContext();
var featureTitle = ctx.Feature?.Title; // "User Authentication"
}

IDisposable​

FeatureTest implements IDisposable. When the test completes, Dispose() flushes the formatted output summary to ITestOutputHelper. The summary includes:

  • Feature title and description
  • Scenario name
  • All steps with pass/fail status
  • Step count and elapsed time
Feature: Shopping Cart

Scenario: Free shipping for large orders
Given an order totalling 150.00 dollars
Then shipping is Free

✓ 2 passing (5ms)
caution

Do not override Dispose() without calling base.Dispose() — this will suppress the formatted output.


Usage​

Basic: Simple scenario​

using SweDevTools.LiveDoc.xUnit;
using Xunit;
using Xunit.Abstractions;

[Feature("Calculator")]
public class CalculatorTests : FeatureTest
{
private int _result;

public CalculatorTests(ITestOutputHelper output) : base(output) { }

[Scenario]
public void Adding_two_numbers()
{
Given("the number '5'", ctx =>
{
_result = ctx.Step!.Values[0].AsInt();
});

When("I add '3'", ctx =>
{
_result += ctx.Step!.Values[0].AsInt();
});

Then("the result is '8'", ctx =>
{
Assert.Equal(ctx.Step!.Values[0].AsInt(), _result);
});
}
}

Scenario with named display title​

Use the nameof pattern or string titles to control Test Explorer display names:

[Scenario(nameof(User_logs_in_with_valid_credentials))]
public void User_logs_in_with_valid_credentials()
{
Given("a registered user with email 'alice@example.com'", ctx =>
{
_user = CreateUser(ctx.Step!.Values[0].AsString());
});

When("they submit valid credentials", () =>
{
_loginResult = _authService.Login(_user.Email, "password123");
});

Then("login succeeds", () =>
{
Assert.True(_loginResult.Success);
});
}

ScenarioOutline with Example data​

[ScenarioOutline]
[Example("Australia", 100.00, "Free")]
[Example("Australia", 49.99, "Standard")]
[Example("New Zealand", 200.00, "International")]
public void Shipping_for_COUNTRY_costing_TOTAL_is_TYPE(
string country, decimal total, string type)
{
Given("a customer from <country>", () =>
{
_cart = new ShoppingCart { Country = country };
});

When("their order totals <total>", () =>
{
_cart.AddItem(new CartItem { Price = total });
_cart.Calculate();
});

Then("shipping type is <type>", () =>
{
Assert.Equal(type, _cart.ShippingType);
});
}

Async steps​

[Scenario]
public async Task Fetch_user_profile()
{
await Given("an authenticated user 'alice'", async ctx =>
{
var username = ctx.Step!.Values[0].AsString();
_token = await _authService.AuthenticateAsync(username);
});

await When("requesting the profile", async () =>
{
_profile = await _api.GetProfileAsync(_token);
});

Then("the profile is returned", () =>
{
Assert.NotNull(_profile);
});
}

Using Example dynamic property​

In [ScenarioOutline] methods, access the current row's data via the Example dynamic property:

[ScenarioOutline]
[Example("Premium", 0.10)]
[Example("Standard", 0.00)]
public void Discount_for_membership(string tier, double discount)
{
Given("a <tier> member", () =>
{
_membership = new Membership((string)Example.tier);
});

Then("discount is <discount>", () =>
{
Assert.Equal((double)Example.discount, _membership.DiscountRate);
});
}

Feature with description​

[Feature("Order Processing", Description = @"
Keeps confirmation predictable for standard
orders when they are submitted.")]
public class OrderTests : FeatureTest
{
public OrderTests(ITestOutputHelper output) : base(output) { }

[Scenario(Description = "Assumes a valid order; payment and fulfillment need separate coverage.")]
public void Standard_order_flow()
{
Given("a valid order", () => { /* ... */ });
When("the order is submitted", () => { /* ... */ });
Then("the order is confirmed", () => { /* ... */ });
}
}

Formatted Output​

Every test produces structured, readable output in the Test Explorer detail pane:

Feature: Order Processing
End-to-end order processing workflow including
validation, payment, and fulfillment steps.

Scenario: Standard order flow
Given a valid order
When the order is submitted
Then the order is confirmed

✓ 3 passing (8ms)

For ScenarioOutline, each example row produces its own output block:

Feature: Shipping Costs

Scenario Outline: Shipping for Australia costing 100.00 is Free
Given a customer from Australia
When their order totals 100.00
Then shipping type is Free

✓ 3 passing (4ms)

See Also​