Spensara Spensara Knowledge Base & Documentation Repository

Repository Docs & Knowledge Base

Consolidated Spensara documentation repository (722 Markdown & architecture documents in ../repository)
Built Word Documents (.docx): Spensara Overview (.docx) Capability Assessment (.docx) Brink POS Ingestion Analysis (.docx) The Borg Ingestion Architecture (.docx)

Agent Tooling (71)

Automation workflows, orchestration references, and developer tooling guides.
agent-tooling/.agents/skills/tunit-testing/references/data-driven.md 10,437 bytes · Updated Oct 23, 2026 10:44 AM EDT
Raw File

Data-Driven Testing

Comprehensive guide to data-driven testing patterns in TUnit.

Table of Contents

Basic Arguments

Use [Arguments(...)] for compile-time constant values:

csharp
[Test]
[Arguments(2, 3, 5)]
[Arguments(10, 20, 30)]
[Arguments(-1, 1, 0)]
public async Task Add_WithNumbers_ReturnsSum(int a, int b, int expected)
{
    var result = Calculator.Add(a, b);
    await Assert.That(result).IsEqualTo(expected);
}

With Display Names

csharp
[Test]
[Arguments(1, 1, 2, DisplayName = "One plus one equals two")]
[Arguments(0, 0, 0, DisplayName = "Zero plus zero equals zero")]
[Arguments(-1, 1, 0, DisplayName = "Negative and positive cancel out")]
public async Task Addition(int a, int b, int expected)
{
    await Assert.That(a + b).IsEqualTo(expected);
}

Display names support parameter substitution:

csharp
[Arguments(2, 3, 5, DisplayName = "Adding $a + $b = $expected")]
[Arguments(10, 5, 15, DisplayName = "$arg1 + $arg2 = $arg3")]

With Categories

csharp
[Test]
[Arguments(100, 50, Categories = new[] { "LargeNumbers", "Performance" })]
[Arguments(1, 1, Categories = new[] { "SmallNumbers", "Smoke" })]
public async Task CategorizedTests(int a, int b)
{
    await Assert.That(a + b).IsGreaterThan(0);
}

Skipping Test Cases

csharp
[Test]
[Arguments("Chrome", "120")]
[Arguments("Firefox", "121")]
[Arguments("Safari", "17", Skip = "Safari testing not available in CI")]
public async Task BrowserTest(string browser, string version)
{
    // Test implementation
}

Method Data Sources

For dynamic data or complex objects, use [MethodDataSource]:

Simple Objects

csharp
public record AdditionTestData(int Value1, int Value2, int ExpectedResult);

public static class TestDataSources
{
    public static IEnumerable<Func<AdditionTestData>> AdditionData()
    {
        yield return () => new AdditionTestData(1, 2, 3);
        yield return () => new AdditionTestData(2, 2, 4);
        yield return () => new AdditionTestData(5, 5, 10);
    }
}

public class CalculatorTests
{
    [Test]
    [MethodDataSource(typeof(TestDataSources), nameof(TestDataSources.AdditionData))]
    public async Task Add_WithTestData_ReturnsExpected(AdditionTestData data)
    {
        var result = Calculator.Add(data.Value1, data.Value2);
        await Assert.That(result).IsEqualTo(data.ExpectedResult);
    }
}

Important: Return Func<T> (not just T) to ensure each test gets a fresh instance.

Using Tuples

csharp
public static class TestDataSources
{
    public static IEnumerable<Func<(int, int, int)>> AdditionData()
    {
        yield return () => (1, 2, 3);
        yield return () => (2, 2, 4);
        yield return () => (5, 5, 10);
    }
}

[Test]
[MethodDataSource(typeof(TestDataSources), nameof(TestDataSources.AdditionData))]
public async Task Add_WithTuples_ReturnsSum(int a, int b, int expected)
{
    var result = Calculator.Add(a, b);
    await Assert.That(result).IsEqualTo(expected);
}

Same-Class Data Source

If the data source is in the same class, omit the type parameter:

csharp
public class CalculatorTests
{
    public static IEnumerable<Func<(int, int, int)>> AdditionData()
    {
        yield return () => (1, 2, 3);
        yield return () => (2, 2, 4);
    }

    [Test]
    [MethodDataSource(nameof(AdditionData))]
    public async Task Add_ReturnsSum(int a, int b, int expected)
    {
        var result = Calculator.Add(a, b);
        await Assert.That(result).IsEqualTo(expected);
    }
}

Async Data Sources

Use IAsyncEnumerable<T> for async data loading (e.g., from database or API):

csharp
public record AsyncTestData(int Id, string Name, DateTime CreatedAt);

public static class AsyncDataSources
{
    public static async IAsyncEnumerable<Func<AsyncTestData>> GetAsyncData(
        [EnumeratorCancellation] CancellationToken ct = default)
    {
        for (int i = 1; i <= 3; i++)
        {
            ct.ThrowIfCancellationRequested();
            
            // Simulate async data loading
            await Task.Delay(10, ct);
            
            yield return () => new AsyncTestData(
                Id: i,
                Name: $"Item_{i}",
                CreatedAt: DateTime.UtcNow.AddDays(-i)
            );
        }
    }
}

[Test]
[MethodDataSource(typeof(AsyncDataSources), nameof(AsyncDataSources.GetAsyncData))]
public async Task TestWithAsyncData(AsyncTestData data)
{
    await Assert.That(data.Id).IsGreaterThan(0);
    await Assert.That(data.Name).StartsWith("Item_");
}

Test Case Metadata

Use TestDataRow for rich metadata on data-driven tests:

csharp
public static IEnumerable<TestDataRow<int, int, int>> AdditionData()
{
    yield return new TestDataRow<int, int, int>(1, 2, 3)
    {
        DisplayName = "Simple addition",
        Categories = new[] { "Basic" }
    };
    
    yield return new TestDataRow<int, int, int>(100, 200, 300)
    {
        DisplayName = "Large numbers",
        Categories = new[] { "Performance" }
    };
    
    yield return new TestDataRow<int, int, int>(int.MaxValue, 1, int.MinValue)
    {
        DisplayName = "Overflow scenario",
        Skip = "Not handling overflow yet"
    };
}

[Test]
[MethodDataSource(nameof(AdditionData))]
public async Task Add_WithMetadata(int a, int b, int expected)
{
    var result = Calculator.Add(a, b);
    await Assert.That(result).IsEqualTo(expected);
}

Matrix Testing

Test all combinations of multiple data sets:

csharp
public class MatrixTests
{
    public static IEnumerable<int> Numbers() => new[] { 1, 2, 3 };
    public static IEnumerable<string> Operations() => new[] { "add", "subtract" };

    [Test]
    [MatrixData(nameof(Numbers), nameof(Operations))]
    public async Task CombinationTest(int number, string operation)
    {
        // Tests all combinations:
        // (1, "add"), (1, "subtract"), (2, "add"), (2, "subtract"), (3, "add"), (3, "subtract")
        await Assert.That(number).IsGreaterThan(0);
        await Assert.That(operation).IsNotEmpty();
    }
}

Common Patterns

Testing HTTP Status Codes

csharp
public static IEnumerable<Func<(string, int)>> EndpointTestData()
{
    yield return () => ("/alerts", 200);
    yield return () => ("/alerts/123", 200);
    yield return () => ("/alerts/invalid", 404);
    yield return () => ("/unauthorized", 401);
}

[Test]
[MethodDataSource(nameof(EndpointTestData))]
public async Task Endpoint_ReturnsExpectedStatus(string path, int expectedStatus)
{
    var client = Factory.CreateClient();
    var response = await client.GetAsync(path);
    
    await Assert.That((int)response.StatusCode).IsEqualTo(expectedStatus);
}

Testing Validation Rules

csharp
public record ValidationTestCase(string Input, bool IsValid, string? ErrorMessage);

public static IEnumerable<Func<ValidationTestCase>> ValidationData()
{
    yield return () => new ValidationTestCase("valid@email.com", true, null);
    yield return () => new ValidationTestCase("invalid", false, "Invalid email format");
    yield return () => new ValidationTestCase("", false, "Email is required");
}

[Test]
[MethodDataSource(nameof(ValidationData))]
public async Task Validate_Email(ValidationTestCase testCase)
{
    var result = EmailValidator.Validate(testCase.Input);
    
    await Assert.That(result.IsValid).IsEqualTo(testCase.IsValid);
    
    if (!testCase.IsValid)
    {
        await Assert.That(result.ErrorMessage).IsEqualTo(testCase.ErrorMessage);
    }
}

Database Test Data

csharp
public static async IAsyncEnumerable<Func<UserTestData>> GetUserTestData(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    // Load from database or API
    var users = await LoadTestUsersAsync(ct);
    
    foreach (var user in users)
    {
        yield return () => new UserTestData(user.Id, user.Name, user.Email);
    }
}

[Test]
[MethodDataSource(nameof(GetUserTestData))]
public async Task User_CanBeProcessed(UserTestData userData)
{
    await using var scope = Factory.Server.Services.CreateAsyncScope();
    var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
    
    var result = await userService.ProcessUserAsync(userData.Id);
    
    await Assert.That(result).IsNotNull();
}

Complex Request/Response Testing

csharp
public record ApiTestCase(
    string Description,
    object RequestBody,
    int ExpectedStatus,
    Action<object?>? ResponseValidator = null
);

public static IEnumerable<Func<ApiTestCase>> CreateAlertTestCases()
{
    yield return () => new ApiTestCase(
        "Valid alert creation",
        new CreateAlertRequest { Name = "Test", Threshold = 100 },
        201,
        response => {
            var alert = response as AlertResponse;
            Assert.That(alert).IsNotNull();
            Assert.That(alert!.Name).IsEqualTo("Test");
        }
    );
    
    yield return () => new ApiTestCase(
        "Missing required fields",
        new CreateAlertRequest { Name = "" },
        400,
        null
    );
}

[Test]
[MethodDataSource(nameof(CreateAlertTestCases))]
public async Task CreateAlert_TestCase(ApiTestCase testCase)
{
    var client = Factory.CreateClient();
    var response = await client.PostAsJsonAsync("/alerts", testCase.RequestBody);
    
    await Assert.That((int)response.StatusCode).IsEqualTo(testCase.ExpectedStatus);
    
    if (testCase.ResponseValidator != null)
    {
        var content = await response.Content.ReadFromJsonAsync<object>();
        testCase.ResponseValidator(content);
    }
}

Best Practices

  1. Use Func<T> for reference types - Ensures each test gets a fresh instance
  2. Keep test data centralized - Put data sources in shared classes or same test class
  3. Use meaningful display names - Makes test reports readable
  4. Apply categories strategically - Enable selective test execution
  5. Skip intelligently - Document why tests are skipped
  6. Prefer tuples for simple cases - Avoid creating types for trivial data
  7. Use async sources for I/O - Don't block on async operations
  8. Test edge cases - Include boundary values, nulls, empty collections