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
IDataSourceAttributeto 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
| Feature | MatrixDataSource | CombinedDataSources |
|---|---|---|
| Parameter-level attributes | [Matrix] only | ANY IDataSourceAttribute |
| Combination strategy | Cartesian product | Cartesian product |
| Data source types | Matrix-specific | All TUnit data sources |
| Use case | Simple matrix combinations | Complex, 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
| Attribute | Description | Example |
|---|---|---|
[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:
| Parameters | Values Each | Total Tests |
|---|---|---|
| 2 | 3 | 9 |
| 3 | 3 | 27 |
| 4 | 3 | 81 |
| 5 | 3 | 243 |
| 3 | 10 | 1,000 |
| 4 | 10 | 10,000 |
Optimization Tips
- Reduce parameter value sets when possible
- Use focused test methods - test one concept per method
- Consider using
[Matrix]for simpler scenarios if you don't need mixed data sources - 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
- MatrixDataSource Documentation
- MethodDataSource Documentation
- ClassDataSource Documentation
- Arguments Attribute Documentation
Version History
- v1.0.0 - Initial release
- Parameter-level data source support
- Cartesian product generation
- Support for all
IDataSourceAttributeimplementations - Full AOT compatibility