Learnixo
Back to blog
AI Systemsintermediate

Theory and InlineData — Parameterized Tests in xUnit

Write parameterized tests with xUnit Theory: InlineData, MemberData, ClassData, TheoryData, and the data-driven testing patterns that eliminate repetitive test boilerplate.

Asma Hafeez KhanMay 16, 20264 min read
TestingxUnitTheory.NETUnit Testing
Share:𝕏

Why Parameterized Tests

A single code path tested with 10 different inputs requires 10 [Fact] methods — identical structure, different data. [Theory] with data attributes collapses this into one test method plus a data set.

C#
// Before — 4 identical tests
[Fact] public void Dosage_zero_amount_should_fail() { ... }
[Fact] public void Dosage_negative_amount_should_fail() { ... }
[Fact] public void Dosage_invalid_unit_should_fail() { ... }
[Fact] public void Dosage_null_unit_should_fail() { ... }

// After — one Theory
[Theory]
[InlineData(0,    "mg",    "Dosage.AmountMustBePositive")]
[InlineData(-1,   "mg",    "Dosage.AmountMustBePositive")]
[InlineData(100,  "pints", "Dosage.InvalidUnit")]
[InlineData(100,  null,    "Dosage.UnitRequired")]
public void Dosage_with_invalid_values_should_fail(
    decimal amount, string? unit, string expectedCode) { ... }

[InlineData] — Inline Literal Data

C#
[Theory]
[InlineData("",        "Patient.NameRequired")]
[InlineData("   ",     "Patient.NameRequired")]    // whitespace-only
[InlineData(null,      "Patient.NameRequired")]
[InlineData("X",       "Patient.NameTooShort")]    // 1 char
[InlineData("A very very long name that exceeds the allowed maximum of 100 characters in our system boundary validation rules", "Patient.NameTooLong")]
public void Create_with_invalid_name_should_fail(string? name, string expectedCode)
{
    var dob    = new DateOnly(1985, 3, 15);
    var result = Patient.Create(name!, dob, "MRN-001");

    result.IsFailure.Should().BeTrue();
    result.Error.Code.Should().Be(expectedCode);
}

Limitations of [InlineData]:

  • Only supports constants (no new DateTime(), no objects)
  • Parameters must be literal values: strings, ints, bools, enums

[MemberData] — Data from Static Members

C#
// Test class with MemberData property
public class PrescriptionTests
{
    public static IEnumerable<object[]> InvalidDosages =>
    [
        [0m,   "mg",    "Dosage.AmountMustBePositive"],
        [-10m, "mg",    "Dosage.AmountMustBePositive"],
        [100m, "pints", "Dosage.InvalidUnit"],
    ];

    [Theory]
    [MemberData(nameof(InvalidDosages))]
    public void Dosage_invalid_should_fail(
        decimal amount, string unit, string expectedCode)
    {
        var result = Dosage.Create(amount, unit);
        result.Error.Code.Should().Be(expectedCode);
    }
}

// Data in a separate class (reusable)
public static class TestData
{
    public static IEnumerable<object[]> ValidPatients =>
    [
        [new DateOnly(1985, 3, 15), "John Smith",  "MRN-001"],
        [new DateOnly(1990, 7, 22), "Jane Doe",    "MRN-002"],
        [new DateOnly(1960, 11, 1), "Robert Jones","MRN-003"],
    ];
}

[Theory]
[MemberData(nameof(TestData.ValidPatients), MemberType = typeof(TestData))]
public void Create_valid_patient_should_succeed(
    DateOnly dob, string name, string mrn) { /* ... */ }

[ClassData] — Data from an IEnumerable Class

C#
// ClassData class — implements IEnumerable<object[]>
public class ValidWardCodes : IEnumerable<object[]>
{
    private static readonly string[] Codes =
    [
        "WARD-A1", "WARD-B2", "ICU-01", "ED-TRAUMA", "PEDS-03"
    ];

    public IEnumerator<object[]> GetEnumerator()
        => Codes.Select(c => new object[] { c }).GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

[Theory]
[ClassData(typeof(ValidWardCodes))]
public void Ward_code_valid_format_should_be_accepted(string code)
{
    var result = WardCode.Create(code);
    result.IsSuccess.Should().BeTrue();
}

TheoryData — Type-Safe Theory Data (xUnit v2.5+)

C#
// TheoryData<T1, T2> is type-safe — no object[] boxing
public static TheoryData<decimal, string, string> DosageData =>
    new()
    {
        { 0m,    "mg",    "Dosage.AmountMustBePositive" },
        { -1m,   "mg",    "Dosage.AmountMustBePositive" },
        { 100m,  "pints", "Dosage.InvalidUnit" },
    };

[Theory]
[MemberData(nameof(DosageData))]
public void Dosage_invalid_should_fail(
    decimal amount, string unit, string expectedCode)
{
    // Type-safe — decimal, string, string guaranteed by TheoryData
    var result = Dosage.Create(amount, unit);
    result.Error.Code.Should().Be(expectedCode);
}

TheoryData is preferred over object[] — type mismatches are compile-time errors.


Testing Boundary Values

C#
// Boundary value testing: exact limits + just outside
public static TheoryData<int, bool> AgeBoundaries =>
    new()
    {
        { -1,  false },  // below minimum (unborn)
        { 0,   true  },  // minimum valid (newborn)
        { 17,  true  },  // just below adult threshold
        { 18,  true  },  // adult threshold
        { 150, false },  // maximum (above known human lifespan)
        { 151, false },  // above maximum
    };

[Theory]
[MemberData(nameof(AgeBoundaries))]
public void Patient_age_boundary_validation(int years, bool expectedValid)
{
    var dob    = DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-years));
    var result = Patient.Create("Test Patient", dob, "MRN-TEST");

    if (expectedValid)
        result.IsSuccess.Should().BeTrue();
    else
        result.IsFailure.Should().BeTrue();
}

Skip Individual Cases

C#
[Theory]
[InlineData("valid-case",  true)]
[InlineData("edge-case",   true,  Skip = "Known bug — see issue #123")]
[InlineData("invalid-case",false)]
public void Theory_with_skipped_case(string input, bool expected) { ... }

Naming Theory Tests in Output

xUnit names theory test runs as MethodName(param1, param2) in the test output. With [InlineData]:

DosageTests.Dosage_with_invalid_values_should_fail(amount: 0, unit: "mg", expectedCode: "Dosage.AmountMustBePositive")
DosageTests.Dosage_with_invalid_values_should_fail(amount: -1, unit: "mg", expectedCode: "Dosage.AmountMustBePositive")

Make parameter names descriptive — they appear in CI output on failure.


Red Flag / Green Answer

Red Flag: "We have a single [Fact] test that loops through an array of test cases and asserts inside the loop."

A loop inside a test is a hidden parameterized test. If case 3 fails, the test stops — you do not see whether case 4 and 5 also fail. [Theory] runs each case independently, reports each failure separately, and continues through all cases.

Green Answer:

[Theory] with [InlineData] or TheoryData. Each case runs independently. All failures are reported. Test output names each case by its parameters.


Key Takeaway

[Theory] with data attributes collapses repeated test methods into a single parameterized test. Use [InlineData] for literal constants, [MemberData] for complex objects or reusable data sets, [ClassData] for encapsulated enumerable data, and TheoryData<T> for type-safe parameters without object[] boxing. Run boundary values, null inputs, and edge cases as theory rows — not as separate [Fact] methods.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.