Skip to content

Tabular Attributes

Aryeh Citron edited this page May 9, 2026 · 7 revisions

Tabular Attributes

Added in v2.32.0 | Auto-verify & TUnit support added in v2.33.0

Tabular attributes let you define data-driven tests with typed input and output rows directly on your test methods — no extra data classes, no CSV files, no ceremony. Column names are declared inline, rows are positionally aligned, and output verification is built in.


Quick Start (xUnit v3)

using TestTrackingDiagrams.TabularAttributes;

public class GreetingTests
{
    [Theory]
    [HeadIn("Name",  "Age"), HeadOut("Message")]
    [Inputs("Alice", 30),    Outputs("Hello Alice")]
    [Inputs("Bob",   25),    Outputs("Hello Bob")]
    public void Greeting_matches_name(
        TabularInputs<Person> inputs, TabularOutputs<Greeting> outputs)
    {
        foreach (var person in inputs)
        {
            var result = Greet(person);
            outputs.RecordActualResult(result);
        }
        // No explicit Verify() needed — auto-verified on disposal (xUnit v3 and TUnit)
        // For other frameworks, call outputs.Verify() or use `using var outputs = ...`
    }

    private Greeting Greet(Person p) => new() { Message = $"Hello {p.Name}" };
}

public class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

public class Greeting
{
    public string Message { get; set; } = "";
}

How it works

  1. [HeadIn("Name", "Age")] declares the input column names and acts as the data source (replaces [InlineData]/[MemberData]).
  2. [HeadOut("Message")] declares the output column names.
  3. [Inputs("Alice", 30)] defines one row of input data. Add as many as you need.
  4. [Outputs("Hello Alice")] defines the corresponding expected output row.
  5. TabularInputs<Person> is an IReadOnlyList<Person> — iterate with foreach to get per-row diagram delimiters.
  6. TabularOutputs<Greeting> collects actual results via RecordActualResult() and auto-verifies on disposal — no explicit Verify() needed on xUnit v3 and TUnit.

Attributes

Core Attributes (in TestTrackingDiagrams package)

Attribute Target AllowMultiple Purpose
[Inputs(...)] Method Yes One row of input values (positional, matching [HeadIn] columns)
[Outputs(...)] Method Yes One row of expected output values (positional, matching [HeadOut] columns)
[HeadOut(...)] Method No Declares output column names

Framework-Specific [HeadIn] (separate packages)

Framework Package Base Type
xUnit v3 TestTrackingDiagrams.xUnit3 Xunit.v3.DataAttribute
xUnit v2 TestTrackingDiagrams.xUnit2 Xunit.Sdk.DataAttribute
NUnit 4 TestTrackingDiagrams.NUnit4 NUnitAttribute, ITestBuilder
MSTest TestTrackingDiagrams.MSTest Attribute, ITestDataSource
TUnit TestTrackingDiagrams.TUnit Attribute, IDataSourceAttribute

All [HeadIn] attributes live in namespace TestTrackingDiagrams.TabularAttributes, so a single using covers everything.


Data Types

TabularInputs<T>

IReadOnlyList<T> containing deserialized input rows. Iterating with foreach automatically emits per-row diagram delimiters (hnote annotations that visually separate each row's interactions in the sequence diagram).

// Access by index
var first = inputs[0];

// Iterate (emits diagram delimiters)
foreach (var person in inputs)
{
    await sut.Process(person);
}

TabularOutputs<T>

IReadOnlyList<T> containing expected output rows. Implements IDisposable — disposing auto-verifies if actuals were recorded and Verify() was not already called.

// Record actual output
outputs.RecordActualResult(result);

// Option 1: Auto-verify on disposal (xUnit v3 and TUnit handle this automatically)
// No explicit Verify() needed

// Option 2: Explicit verify (required for xUnit v2, NUnit 4, MSTest)
outputs.Verify(); // throws TabularVerificationException on mismatch

// Option 3: Use `using` for auto-verify on any framework
using var outputs = ...; // Dispose() triggers Verify()

Verification rules:

  • Matching rows: expected and actual at the same position are compared cell by cell.
  • Surplus rows: more actuals than expected → fail.
  • Missing rows: fewer actuals than expected → fail.

Column-to-Property Matching

Column names are matched to T's public properties using a sanitization process:

Column Name Matches Property
"Name" Name
"First Name" FirstName (spaces removed)
"Name & Age" NameAndAge (&And)
"username" USERNAME (case-insensitive)

If [HeadIn] is used without arguments ([HeadIn]), column names are inferred from T's public property names.


Framework Usage

xUnit v3

[Theory]
[HeadIn("Name", "Age")]
[Inputs("Alice", 30)]
public void My_test(TabularInputs<Person> inputs) { ... }

xUnit v2

[Theory]
[HeadIn("Name", "Age")]
[Inputs("Alice", 30)]
public void My_test(TabularInputs<Person> inputs) { ... }

NUnit 4

[Test]
[HeadIn("Name", "Age")]
[Inputs("Alice", 30)]
public void My_test(TabularInputs<Person> inputs) { ... }

MSTest

[TestMethod]
[HeadIn("Name", "Age")]
[Inputs("Alice", 30)]
public void My_test(TabularInputs<Person> inputs) { ... }

TUnit

[Test]
[HeadIn("Name", "Age")]
[Inputs("Alice", 30)]
public void My_test(TabularInputs<Person> inputs) { ... }

Step Tracking Integration

When TabularInputs<T> or TabularOutputs<T> is passed as a step parameter, StepCollector automatically detects it and renders it as a tabular parameter in the report (instead of inline text):

Track.That.Given("the test data", "inputs", inputs);

This produces a table in the step parameters showing all rows and columns.


Auto-Verify on Disposal

TabularOutputs<T> implements IDisposable. When disposed, it automatically calls Verify() if:

  • At least one actual result was recorded via RecordActualResult(), and
  • Verify() was not already called explicitly.

Frameworks with automatic disposal

Framework Mechanism User action needed
xUnit v3 DisposalTracker — xUnit3 disposes test data after the test None
TUnit TestBuilderContext.Events.OnDispose — TUnit calls disposal hooks None

Frameworks requiring explicit verify

Framework Recommended approach
xUnit v2 Call outputs.Verify() explicitly
NUnit 4 Call outputs.Verify() explicitly
MSTest Call outputs.Verify() explicitly

On any framework, you can also use using to trigger auto-verify:

// Works on all frameworks
using var outputs = tabularOutputs;
foreach (var input in inputs)
{
    outputs.RecordActualResult(Process(input));
}
// Verify() called automatically when `using` scope ends

Inputs Only (No Outputs)

You can use TabularInputs<T> without outputs:

[Theory]
[HeadIn("Name", "Age")]
[Inputs("Alice", 30)]
[Inputs("Bob",   25)]
public void Process_people(TabularInputs<Person> inputs)
{
    foreach (var person in inputs)
    {
        sut.Process(person);
    }
}

Outputs Only (No Inputs)

You can also use only TabularOutputs<T>:

[Theory]
[HeadIn, HeadOut("Name", "Status")]
[Outputs("Alice", "Active")]
[Outputs("Bob",   "Inactive")]
public void Verify_results(TabularOutputs<UserStatus> outputs)
{
    outputs.AddActual(new UserStatus { Name = "Alice", Status = "Active" });
    outputs.AddActual(new UserStatus { Name = "Bob", Status = "Inactive" });
    outputs.Verify();
}

Performance Considerations

Tabular attributes batch multiple data rows into a single test execution. This is useful when:

  • You want to verify a batch operation processes all rows correctly.
  • You want per-row diagram delimiters showing each row's interactions.
  • You want to compare expected vs actual outputs positionally.

For truly independent test cases (where each row should be a separate test run with its own pass/fail), use the framework's native parameterisation ([InlineData], [TestCase], etc.) instead.


Related

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally