Skip to main content

Value Extraction API

The value extraction API provides type-safe access to data embedded in step and rule titles. Quoted values ('value') are accessed via Values, named parameters (<name:value>) via Params. Both support strong typing through AsInt(), AsString(), AsBool(), and tuple deconstruction.

// Step-level extraction (in FeatureTest)
Given("a user with '100' credits and status 'active'", ctx =>
{
var credits = ctx.Step!.Values[0].AsInt(); // 100
var status = ctx.Step!.Values[1].AsString(); // "active"
});

// Rule-level extraction (in SpecificationTest)
[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);
}

Where Values Come From

Values are extracted from two syntaxes in title strings:

SyntaxExtraction APIExample TitleAccess
'value' (quoted)Values[index]"a cart with '5' items"Values[0]5
<name:value> (named)Params["name"]"user <age:25> years"Params["age"]25

Both syntaxes work identically on steps (via ctx.Step) and rules (via Rule):

ContextValuesParams
Step (in FeatureTest)ctx.Step!.Values[i]ctx.Step!.Params["name"]
Rule (in SpecificationTest)Rule.Values[i]Rule.Params["name"]

Reference

LiveDocValue

A type-safe wrapper around a single extracted value. Provides conversion methods for common types.

LiveDocValue value = ctx.Step!.Values[0];
int num = value.AsInt();
string str = value.AsString();

Conversion Methods

MethodReturnsExample InputResult
.AsString()string'hello'"hello"
.AsInt()int'42'42
.AsLong()long'9999999999'9999999999L
.AsDecimal()decimal'19.99'19.99m
.AsDouble()double'3.14'3.14d
.AsBool()bool'true'true
.AsDateTime()DateTime'2024-01-15'DateTime(2024,1,15)
.As<T>()TvariesConverts to any parseable type

Properties

PropertyTypeDescription
RawstringThe unconverted string value as it appeared in the title (without quotes).

Type Coercion Rules

  • All numeric conversions use CultureInfo.InvariantCulture (decimal point is .).
  • .AsBool() accepts "true" / "false" (case-insensitive).
  • .AsDateTime() parses ISO 8601 and common date formats.
  • .As<T>() supports:
    • Enums: 'Active'Status.Active
    • Guid: '550e8400-e29b-...'Guid
    • Any IConvertible type
Given("status is 'Active'", ctx =>
{
var status = ctx.Step!.Values[0].As<OrderStatus>(); // Enum conversion
Assert.Equal(OrderStatus.Active, status);
});

Error Handling

If conversion fails, a LiveDocConversionException is thrown with the step title for context:

LiveDocConversionException: Cannot convert 'abc' to Int32.
Step: "a cart with 'abc' items"

LiveDocValueArray

Bounds-checked array of LiveDocValue instances, extracted from all quoted values in a title.

LiveDocValueArray values = ctx.Step!.Values;

Indexer

LiveDocValue this[int index]

Returns the value at the given position. Throws LiveDocValueIndexException if the index is out of bounds.

Given("adding '5' and '3'", ctx =>
{
var a = ctx.Step!.Values[0].AsInt(); // 5
var b = ctx.Step!.Values[1].AsInt(); // 3
});

Properties

PropertyTypeDescription
LengthintNumber of extracted values.
CountintSame as Length.

Tuple Deconstruction — As<T1, T2, ...>()

Converts all values to a strongly-typed tuple in a single call. Supports 2 to 6 type parameters:

// Two values
var (name, age) = ctx.Step!.Values.As<string, int>();

// Three values
var (a, b, expected) = Rule.Values.As<int, int, int>();

// Four values
var (country, total, shipping, tax) =
ctx.Step!.Values.As<string, decimal, string, decimal>();

// Five values
var (v1, v2, v3, v4, v5) =
ctx.Step!.Values.As<string, int, bool, double, long>();

// Six values
var (v1, v2, v3, v4, v5, v6) =
ctx.Step!.Values.As<string, int, bool, double, long, decimal>();
caution

The number of type parameters must match the number of quoted values in the title. If the title has 3 quoted values but you call .As<T1, T2>(), a LiveDocValueIndexException is thrown.

ToArray()

LiveDocValue[] ToArray()

Returns all values as a standard array for iteration:

Given("items: '10', '20', '30'", ctx =>
{
var items = ctx.Step!.Values.ToArray();
foreach (var item in items)
{
_cart.AddItem(item.AsInt());
}
});

LiveDocValueDictionary

Dictionary of named LiveDocValue instances, extracted from <name:value> syntax in titles.

LiveDocValueDictionary params = ctx.Step!.Params;

Indexer

LiveDocValue this[string name]

Returns the value for the given parameter name. Throws LiveDocParamNotFoundException if the name doesn't exist.

Given("a user with <email:alice@test.com> and <role:admin>", ctx =>
{
var email = ctx.Step!.Params["email"].AsString(); // "alice@test.com"
var role = ctx.Step!.Params["role"].AsString(); // "admin"
});

Methods

MethodReturnsDescription
ContainsKey(string name)boolWhether the parameter exists.
TryGetValue(string name, out LiveDocValue value)boolSafe access without exception.
GetEnumerator()IEnumerator<...>Iterate all parameters.

Properties

PropertyTypeDescription
CountintNumber of named parameters.
KeysIEnumerable<string>All parameter names.
ValuesIEnumerable<LiveDocValue>All parameter values.
RawIReadOnlyDictionary<string, string>Raw string key-value pairs.

Raw Access

Both StepContext and RuleContext expose raw (unconverted) string values:

PropertyTypeDescription
ValuesRawstring[]Quoted values as raw strings before conversion.
ParamsRawIReadOnlyDictionary<string, string>Named parameters as raw strings.
Given("a price of '19.99'", ctx =>
{
string raw = ctx.Step!.ValuesRaw[0]; // "19.99" (string)
decimal typed = ctx.Step!.Values[0].AsDecimal(); // 19.99m (decimal)
});

[Rule("Setting <count:5> items")]
public void Example_rule()
{
string raw = Rule.ParamsRaw["count"]; // "5" (string)
int typed = Rule.Params["count"].AsInt(); // 5 (int)
}

Usage

Step values in FeatureTest

[Scenario]
public void Cart_total_with_tax()
{
Given("a cart with '3' items at '9.99' each", ctx =>
{
var (count, price) = ctx.Step!.Values.As<int, decimal>();
for (int i = 0; i < count; i++)
_cart.AddItem(new CartItem { Price = price });
});

When("tax rate is '10' percent", ctx =>
{
_cart.TaxRate = ctx.Step!.Values[0].AsInt() / 100.0m;
_cart.Calculate();
});

Then("the total is '32.97'", ctx =>
{
Assert.Equal(ctx.Step!.Values[0].AsDecimal(), _cart.Total);
});
}

Rule values in SpecificationTest

[Specification("URL Validation")]
public class UrlSpec : SpecificationTest
{
public UrlSpec(ITestOutputHelper output) : base(output) { }

[Rule("'https://example.com' is a valid URL")]
public void Valid_url()
{
var url = Rule.Values[0].AsString();
Assert.True(Uri.IsWellFormedUriString(url, UriKind.Absolute));
}

[Rule("'not-a-url' is not a valid URL")]
public void Invalid_url()
{
var url = Rule.Values[0].AsString();
Assert.False(Uri.IsWellFormedUriString(url, UriKind.Absolute));
}
}

Named parameters for clarity

Named parameters make complex steps self-documenting:

[Scenario]
public void International_shipping()
{
Given("an order from <country:Japan> weighing <weight:2.5> kg", ctx =>
{
_order = new Order
{
Country = ctx.Step!.Params["country"].AsString(),
WeightKg = ctx.Step!.Params["weight"].AsDouble()
};
});

Then("shipping cost is <cost:15.00> dollars", ctx =>
{
var expected = ctx.Step!.Params["cost"].AsDecimal();
Assert.Equal(expected, _order.CalculateShipping());
});
}

Mixed values and params

Quoted values and named parameters can coexist in the same title:

Given("user '42' has <role:admin> access", ctx =>
{
var userId = ctx.Step!.Values[0].AsInt(); // 42 (from '42')
var role = ctx.Step!.Params["role"].AsString(); // "admin" (from <role:admin>)
});

Exceptions

ExceptionCauseInformation
LiveDocConversionExceptionInvalid type conversion (e.g., 'abc'.AsInt())Includes step/rule title
LiveDocValueIndexExceptionValues[n] index out of boundsIncludes RequestedIndex, AvailableCount, step title
LiveDocParamNotFoundExceptionParams["x"] for non-existent keyIncludes ParamName, AvailableParams, step title
// This throws LiveDocValueIndexException:
// "Index 2 requested but only 1 value available in step: 'a cart with '5' items'"
Given("a cart with '5' items", ctx =>
{
var x = ctx.Step!.Values[2]; // ❌ Only index 0 exists
});

See Also