Open
Description
I'd like to propose a combinatorial attribute equivalent to the xUnit ClassDataAttribute
allowing reuse of the same data source values across multiple (combinatorial) tests.
I've implemented and tested the following:
public class CombinatorialClassDataAttribute : Attribute
{
public CombinatorialClassDataAttribute(Type valuesSourceType, params object[]? args)
{
this.Values = GetValues(valuesSourceType, args);
}
/// <inheritdoc />
public object?[] Values { get; }
private static object?[] GetValues(Type valuesSourceType, object[] args)
{
var valuesSource = Activator.CreateInstance(
valuesSourceType,
BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding,
null,
args,
CultureInfo.InvariantCulture) as IEnumerable;
if (valuesSource is null)
{
throw new InvalidOperationException($"The values source must be convertible to {typeof(IEnumerable)}.");
}
if (valuesSource is IEnumerable<object[]> enumerableOfObjects)
{
// This also supports types deriving from TheoryData and TheoryData<T>, which are IEnumerable<object[]>
return enumerableOfObjects.SelectMany(objects => objects).Cast<object?>().ToArray();
}
return valuesSource.Cast<object?>().ToArray();
}
}
In .NET Standard 2.0, there's the option to use generic attributes, which I've implemented like this:
#if NETSTANDARD2_0_OR_GREATER
public class CombinatorialClassDataAttribute<TValueSource> : CombinatorialClassDataAttribute
where TValueSource : class, IEnumerable
{
public CombinatorialClassDataAttribute(params object[]? args)
: base(typeof(TValueSource), args)
{
}
}
#endif
This will allow users to write a test like this:
public record MyTestData(int Number, string Text);
// This can be anything implementing IEnumerable returning the expected type, using TheoryData<T> is just more convenient
public class MySharedTestData : TheoryData<MyTestData>
{
public MySharedTestData(int seed = 0)
{
Add(new(Number: 42 + seed, Text: "Lorem");
Add(new(Number: 123 + seed, Text": "Ipsum");
}
}
public class MyTests
{
[Theory, CombinatorialData]
public void TestMyData([CombinatorialClassData(typeof(MySharedTestData)] MyTestData testCase /* add other combinations of combinatorial data */)
{
// Assert things with testCase
}
[Theory, CombinatorialData]
public void TestMyDataWithGenericAttribute([CombinatorialClassData<MySharedTestData>] MyTestData testCase /* add other combinations of combinatorial data */)
{
// Assert things with testCase
}
[Theory, CombinatorialData]
public void TestMyDataWithGenericAttributeAndSeed([CombinatorialClassData<MySharedTestData>(/* seed */ 42)] MyTestData testCase /* add other combinations of combinatorial data */)
{
// Assert things with testCase
}
}
I think this could be a valuable addition to this great library.