# Data-Driven Testing

Comprehensive guide to data-driven testing patterns in TUnit.

## Table of Contents

- [Basic Arguments](#basic-arguments)
- [Method Data Sources](#method-data-sources)
- [Async Data Sources](#async-data-sources)
- [Test Case Metadata](#test-case-metadata)
- [Matrix Testing](#matrix-testing)
- [Common Patterns](#common-patterns)

## 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
