Skip to main content

CombinedDataSources

Overview

The [CombinedDataSources] attribute enables you to apply different data source attributes to individual parameters, creating test cases through Cartesian product combination. This provides maximum flexibility when you need different parameters to be generated by different data sources.

Key Features

  • ✅ Apply ANY IDataSourceAttribute to individual parameters
  • ✅ Mix [Arguments], [MethodDataSource], [ClassDataSource], and custom data sources
  • ✅ Automatic Cartesian product generation
  • ✅ Full AOT/Native compilation support
  • ✅ Works in both source-generated and reflection modes

Comparison with MatrixDataSource

FeatureMatrixDataSourceCombinedDataSources
Parameter-level attributes[Matrix] onlyANY IDataSourceAttribute
Combination strategyCartesian productCartesian product
Data source typesMatrix-specificAll TUnit data sources
Use caseSimple matrix combinationsComplex, mixed data scenarios

Basic Usage

Simple Arguments Mixing

[Test]
[CombinedDataSources]
public async Task SimpleTest(
[Arguments(1, 2, 3)] int x,
[Arguments("a", "b")] string y)
{
// Creates 3 × 2 = 6 test cases:
// (1, "a"), (1, "b"), (2, "a"), (2, "b"), (3, "a"), (3, "b")

await Assert.That(x).IsIn([1, 2, 3]);
await Assert.That(y).IsIn(["a", "b"]);
}

Mixing Arguments with MethodDataSource

public static IEnumerable<string> GetStrings()
{
yield return "Hello";
yield return "World";
}

[Test]
[CombinedDataSources]
public async Task MixedDataSources(
[Arguments(1, 2)] int x,
[MethodDataSource(nameof(GetStrings))] string y)
{
// Creates 2 × 2 = 4 test cases:
// (1, "Hello"), (1, "World"), (2, "Hello"), (2, "World")

await Assert.That(x).IsIn([1, 2]);
await Assert.That(y).IsIn(["Hello", "World"]);
}

Advanced Scenarios

Three Parameters from Different Sources

public static IEnumerable<int> GetNumbers()
{
yield return 10;
yield return 20;
yield return 30;
}

[Test]
[CombinedDataSources]
public async Task ThreeWayMix(
[Arguments(1, 2)] int x,
[MethodDataSource(nameof(GetNumbers))] int y,
[Arguments(true, false)] bool z)
{
// Creates 2 × 3 × 2 = 12 test cases
await Assert.That(x).IsIn([1, 2]);
await Assert.That(y).IsIn([10, 20, 30]);
await Assert.That(z).IsIn([true, false]);
}

Multiple Data Sources on Same Parameter

You can apply multiple data source attributes to a single parameter - all values will be combined:

[Test]
[CombinedDataSources]
public async Task MultipleSourcesPerParameter(
[Arguments(1, 2)]
[Arguments(3, 4)] int x,
[Arguments("a")] string y)
{
// Creates (2 + 2) × 1 = 4 test cases
// x can be: 1, 2, 3, or 4
await Assert.That(x).IsIn([1, 2, 3, 4]);
await Assert.That(y).IsEqualTo("a");
}

Using ClassDataSource

public class MyTestData
{
public int Value { get; set; }
public string Name { get; set; } = string.Empty;
}

[Test]
[CombinedDataSources]
public async Task WithClassDataSource(
[Arguments(1, 2)] int x,
[ClassDataSource<MyTestData>] MyTestData obj)
{
// Creates 2 × 1 = 2 test cases
await Assert.That(x).IsIn([1, 2]);
await Assert.That(obj).IsNotNull();
}

Complex Type Combinations

[Test]
[CombinedDataSources]
public async Task DifferentTypes(
[Arguments(1, 2)] int intVal,
[Arguments("a", "b")] string stringVal,
[Arguments(1.5, 2.5)] double doubleVal,
[Arguments(true, false)] bool boolVal,
[Arguments('x', 'y')] char charVal)
{
// Creates 2 × 2 × 2 × 2 × 2 = 32 test cases
// All combinations of the parameter values
}

Cartesian Product Behavior

The [CombinedDataSources] generates test cases using Cartesian product - every combination of parameter values is tested.

Example Calculation

Given:

  • Parameter a: [Arguments(1, 2, 3)] → 3 values
  • Parameter b: [Arguments("x", "y")] → 2 values
  • Parameter c: [Arguments(true, false)] → 2 values

Total test cases: 3 × 2 × 2 = 12

Generated combinations:

(1, "x", true), (1, "x", false), (1, "y", true), (1, "y", false),
(2, "x", true), (2, "x", false), (2, "y", true), (2, "y", false),
(3, "x", true), (3, "x", false), (3, "y", true), (3, "y", false)

Supported Data Source Attributes

The following attributes can be applied to parameters with [CombinedDataSources]:

Built-in Attributes

AttributeDescriptionExample
[Arguments]Inline values[Arguments(1, 2, 3)]
[MethodDataSource]Values from a method[MethodDataSource(nameof(GetData))]
[MethodDataSource<T>]Typed method data source[MethodDataSource<MyClass>(nameof(GetData))]
[ClassDataSource<T>]Instance generation[ClassDataSource<MyClass>]
[ClassDataSource]Dynamic type instances[ClassDataSource(typeof(MyClass))]

Custom Data Sources

Any attribute implementing IDataSourceAttribute can be used:

public class CustomDataSourceAttribute : DataSourceGeneratorAttribute<string>
{
protected override IEnumerable<Func<string>> GenerateDataSources(
DataGeneratorMetadata metadata)
{
yield return () => "Custom1";
yield return () => "Custom2";
}
}

[Test]
[CombinedDataSources]
public async Task WithCustomDataSource(
[Arguments(1, 2)] int x,
[CustomDataSource] string y)
{
// Creates 2 × 2 = 4 test cases
}

Best Practices

✅ DO

  • Use descriptive parameter names to make test output clear
  • Keep parameter counts reasonable (< 5 parameters typically)
  • Be mindful of Cartesian product size - 5 params × 10 values each = 100,000 tests!
  • Group related tests in the same test class
  • Use assertions to validate parameter values when debugging

❌ DON'T

  • Don't create excessive test cases - Be aware of exponential growth
  • Don't mix with method-level data sources - Use one approach per test method
  • Don't forget to test edge cases like null values
  • Don't leave parameters without data sources - All parameters must have at least one data source attribute

Performance Considerations

Test Case Growth

Be aware of exponential growth with multiple parameters:

ParametersValues EachTotal Tests
239
3327
4381
53243
3101,000
41010,000

Optimization Tips

  1. Reduce parameter value sets when possible
  2. Use focused test methods - test one concept per method
  3. Consider using [Matrix] for simpler scenarios if you don't need mixed data sources
  4. Leverage test parallelization - TUnit runs tests in parallel by default

Edge Cases and Error Handling

Missing Data Source

// ❌ ERROR: Parameter 'y' has no data source
[Test]
[CombinedDataSources]
public async Task MissingDataSource(
[Arguments(1, 2)] int x,
int y) // No data source attribute!
{
// This will fail during test initialization
}

Error: Parameter 'y' has no data source attributes. All parameters must have at least one IDataSourceAttribute when using [CombinedDataSources].

No Parameters

// ❌ ERROR: No parameters with data sources
[Test]
[CombinedDataSources]
public async Task NoParameters()
{
// This will fail
}

Error: [CombinedDataSources] only supports parameterised tests

Nullable Types

Nullable types are fully supported:

[Test]
[CombinedDataSources]
public async Task NullableTypes(
[Arguments(1, 2, null)] int? nullableInt,
[Arguments("a", null)] string? nullableString)
{
// Creates 3 × 2 = 6 test cases including nulls
if (nullableInt.HasValue)
{
await Assert.That(nullableInt.Value).IsIn([1, 2]);
}
}

Comparison with Other Approaches

vs. Method-Level [Arguments]

Method-Level:

[Test]
[Arguments(1, "a")]
[Arguments(2, "b")]
public async Task OldWay(int x, string y)
{
// Must manually specify every combination
// Only creates 2 test cases
}

MixedParameters:

[Test]
[CombinedDataSources]
public async Task NewWay(
[Arguments(1, 2)] int x,
[Arguments("a", "b")] string y)
{
// Automatically creates all 4 combinations
}

vs. MatrixDataSource

Matrix:

[Test]
[MatrixDataSource]
public async Task MatrixWay(
[Matrix(1, 2)] int x,
[Matrix("a", "b")] string y)
{
// Limited to Matrix attribute only
}

MixedParameters:

[Test]
[CombinedDataSources]
public async Task MixedWay(
[Arguments(1, 2)] int x,
[MethodDataSource(nameof(GetStrings))] string y)
{
// Can mix any data source types!
}

AOT/Native Compilation

[CombinedDataSources] is fully compatible with AOT and Native compilation. The attribute uses proper trimming annotations and works in both source-generated and reflection modes.

Examples from Real-World Scenarios

Testing API Endpoints with Different Configurations

public static IEnumerable<HttpMethod> GetHttpMethods()
{
yield return HttpMethod.Get;
yield return HttpMethod.Post;
yield return HttpMethod.Put;
}

[Test]
[CombinedDataSources]
public async Task ApiEndpoint_ResponseCodes(
[MethodDataSource(nameof(GetHttpMethods))] HttpMethod method,
[Arguments("/api/users", "/api/products")] string endpoint,
[Arguments(200, 404)] int expectedStatusCode)
{
// Tests all combinations of HTTP methods, endpoints, and expected codes
// 3 × 2 × 2 = 12 test cases
}

Database Query Testing

public class QueryParameters
{
public int PageSize { get; set; }
public string SortOrder { get; set; } = string.Empty;
}

[Test]
[CombinedDataSources]
public async Task Database_Pagination(
[Arguments(10, 20, 50)] int pageSize,
[Arguments("asc", "desc")] string sortOrder,
[Arguments(true, false)] bool includeDeleted)
{
// Tests all pagination combinations
// 3 × 2 × 2 = 12 test cases
}

Troubleshooting

Issue: Too Many Test Cases Generated

Problem: Test run takes too long due to exponential growth

Solution:

  • Reduce the number of values per parameter
  • Split into multiple focused test methods
  • Use more specific test scenarios

Issue: Data Source Returns No Values

Problem: A parameter's data source returns an empty enumerable

Solution:

  • Ensure data source methods return at least one value
  • Check that the method is static/accessible
  • Verify method signature matches expected format

Issue: Parameter Type Mismatch

Problem: Data source returns wrong type for parameter

Solution:

  • Ensure data source return type matches parameter type
  • Use typed data sources: [MethodDataSource<MyClass>]
  • Check that generated values can be cast to parameter type

See Also

Version History

  • v1.0.0 - Initial release
    • Parameter-level data source support
    • Cartesian product generation
    • Support for all IDataSourceAttribute implementations
    • Full AOT compatibility