Skip to main content

Value Extraction

Value extraction is the mechanism that keeps your tests honest. By embedding data in step titles and extracting it programmatically, the documentation and the test logic can never drift apart.

Why Value Extraction Matters

Consider these two approaches:

// ❌ BAD: Title says 300, code checks 200 — value drift!
Then("the balance should be '300' dollars", () =>
{
Assert.Equal(200, account.Balance);
});

// ✅ GOOD: Value extracted from the title — always in sync
Then("the balance should be '300' dollars", ctx =>
{
Assert.Equal(ctx.Step!.Values[0].AsDecimal(), account.Balance);
});

When someone reads the test output, they see the balance should be '300' dollars. With extraction, the assertion is guaranteed to check 300 — not some other number buried in the code.


Two Extraction Systems

LiveDoc provides value extraction in two contexts:

ContextPropertyUsed In
Steps (BDD)ctx.Step!.Values / ctx.Step!.ParamsGiven, When, Then, And, But
Rules (Specification)Rule.Values / Rule.Params[Rule] methods

Both use the same LiveDocValue type with identical conversion methods.


Quoted Values: Values[]

Wrap values in single quotes in the title. They're extracted in order:

Given("a cart with '5' items totaling '99.99' dollars", ctx =>
{
var itemCount = ctx.Step!.Values[0].AsInt(); // 5
var total = ctx.Step!.Values[1].AsDecimal(); // 99.99
});

In Rules

[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);
}

Type Conversion Methods

Every LiveDocValue supports these conversion methods:

MethodReturnsExample Input
.AsString()string'hello'"hello"
.AsInt()int'42'42
.AsLong()long'9999999999'9999999999L
.AsDecimal()decimal'99.99'99.99m
.AsDouble()double'3.14159'3.14159d
.AsBool()bool'true'true
.AsDateTime()DateTime'2024-01-15'DateTime(2024,1,15)
.As<T>()TAny parseable type (enums, arrays)

Numeric Conversions

Given("an integer '42' and a decimal '3.14' and a long '9999999999'", ctx =>
{
int intVal = ctx.Step!.Values[0].AsInt();
decimal decVal = ctx.Step!.Values[1].AsDecimal();
long longVal = ctx.Step!.Values[2].AsLong();
});

Boolean Conversion

Given("active is 'true' and archived is 'false'", ctx =>
{
bool active = ctx.Step!.Values[0].AsBool(); // true
bool archived = ctx.Step!.Values[1].AsBool(); // false
});

DateTime Conversion

Given("the order was placed on '2024-01-15'", ctx =>
{
DateTime date = ctx.Step!.Values[0].AsDateTime();
Assert.Equal(new DateTime(2024, 1, 15), date);
});

Enum Conversion with As<T>()

The generic As<T>() method handles any type that can be parsed, including enums:

Given("the day is <day:Monday>", ctx =>
{
DayOfWeek day = ctx.Step!.Params["day"].As<DayOfWeek>();
Assert.Equal(DayOfWeek.Monday, day);
});

Array Conversion

Given("product IDs '[101, 102, 103]'", ctx =>
{
int[] ids = ctx.Step!.Values[0].As<int[]>();
Assert.Equal(3, ids.Length);
Assert.Equal(101, ids[0]);
});

Given("tags '[\"sale\", \"new\", \"featured\"]'", ctx =>
{
string[] tags = ctx.Step!.Values[0].As<string[]>();
Assert.Contains("sale", tags);
});

Tuple Deconstruction

When you have multiple values, deconstruct them into a tuple for cleaner code:

Basic Deconstruction

When("I purchase '3' units of 'Green Tea' for '15.50'", ctx =>
{
var (qty, product, cost) = ctx.Step!.Values;
int quantity = qty.AsInt();
string name = product.AsString();
decimal price = cost.AsDecimal();
});

Typed Tuple Deconstruction with As<T1, T2, ...>()

The most concise approach — converts all values in one call:

When("I purchase '7' units of 'Oolong Tea' for '22.99'", ctx =>
{
var (qty, product, cost) = ctx.Step!.Values.As<int, string, decimal>();
// qty = 7, product = "Oolong Tea", cost = 22.99m
});

This also works with Rule.Values:

[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);
}

Typed tuple deconstruction supports 2 to 6 type parameters:

// Two values
var (a, b) = ctx.Step!.Values.As<int, int>();

// Three values
var (x, y, z) = ctx.Step!.Values.As<string, int, bool>();

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

Named Parameters: Params[]

Use the <name:value> syntax for self-documenting parameters:

Given("a user with <email:john@example.com> and <age:25>", ctx =>
{
string email = ctx.Step!.Params["email"].AsString();
int age = ctx.Step!.Params["age"].AsInt();
});

Display Title

Named parameters are replaced with just their values in the display output:

Original TitleDisplay Title
"a user with <email:john@test.com> and <age:25>""a user with john@test.com and 25"

Case Insensitivity

Parameter lookup is case-insensitive:

Given("a parameter <MyParam:test-value>", ctx =>
{
// All of these work:
ctx.Step!.Params["MyParam"].AsString();
ctx.Step!.Params["myparam"].AsString();
ctx.Step!.Params["MYPARAM"].AsString();
});

In Rules

[Rule("Subtracting <b:3> from <a:10> returns <expected:7>")]
public void Subtraction()
{
var a = Rule.Params["a"].AsInt();
var b = Rule.Params["b"].AsInt();
var expected = Rule.Params["expected"].AsInt();
Assert.Equal(expected, a - b);
}

Step Context Properties

The full ctx.Step object exposes several properties:

PropertyTypeDescription
TitlestringThe raw title as written in code
DisplayTitlestringTitle with named params replaced by values
Typestring"Given", "When", "Then", "And", or "But"
ValuesLiveDocValueCollectionIndexed collection of quoted values
Values.CountintNumber of extracted values
ParamsLiveDocParamCollectionNamed parameter dictionary
Params.CountintNumber of extracted parameters
Given("a step with 'value1' and <param:value2>", ctx =>
{
Assert.Equal("a step with 'value1' and <param:value2>", ctx.Step!.Title);
Assert.Equal("a step with 'value1' and value2", ctx.Step!.DisplayTitle);
Assert.Equal("Given", ctx.Step!.Type);
Assert.Equal(1, ctx.Step!.Values.Count);
Assert.Equal(1, ctx.Step!.Params.Count);
});

Error Handling

LiveDoc provides descriptive exceptions when extraction fails:

Invalid Type Conversion

Given("an invalid number 'not-a-number'", ctx =>
{
// Throws LiveDocConversionException with:
// - The value that failed conversion
// - The target type
// - The step title for context
var value = ctx.Step!.Values[0].AsInt(); // throws!
});

Index Out of Range

Given("only one value '42'", ctx =>
{
// Throws LiveDocValueIndexException with:
// - Requested index: 5
// - Available count: 1
// - The step title for context
var value = ctx.Step!.Values[5]; // throws!
});

Missing Parameter

Given("a parameter <existing:value>", ctx =>
{
// Throws LiveDocParamNotFoundException with:
// - The parameter name that was requested
// - A list of available parameter names
var value = ctx.Step!.Params["nonexistent"]; // throws!
});

All exceptions include the step title, making it easy to identify which step caused the failure in test output.

ExceptionCauseFix
LiveDocConversionExceptionInvalid type conversion (e.g., 'abc'.AsInt())Check the quoted value format
LiveDocValueIndexExceptionValues[n] beyond available countVerify the number of quoted values in the title
LiveDocParamNotFoundExceptionParams["x"] for non-existent nameCheck the <name:value> syntax in the title

Combining with ScenarioOutline

You can use value extraction alongside outline placeholders:

[ScenarioOutline("Apply tax multiplier")]
[Example("Australia", 100.00)]
[Example("New Zealand", 50.00)]
public void Apply_tax_multiplier(string country, decimal baseAmount)
{
Given("a customer from <country>", () =>
{
Assert.NotNull(country); // from method parameter
});

When("I apply a multiplier of '1.1' to the base amount", ctx =>
{
var multiplier = ctx.Step!.Values[0].AsDecimal(); // from title
var total = baseAmount * multiplier; // from parameter
Assert.Equal(baseAmount * 1.1m, total);
});
}

Use method parameters for data that varies per example row, and quoted values for constants embedded in the step description.


Recap

  • Values[] — extract quoted 'value' strings by index, with type conversion
  • Params[] — extract named <key:value> parameters by name (case-insensitive)
  • .AsInt() / .AsDecimal() / .AsBool() / etc. — type-safe conversion methods
  • .As<T>() — generic conversion for enums, arrays, and custom types
  • .As<T1, T2, T3>() — typed tuple deconstruction for multiple values
  • Rule.Values / Rule.Params — same API available in Specification rules
  • Error handling — descriptive exceptions with step title context

Next Steps