Skip to content

Add generic combinatorial attributes #95

Open
@siewers

Description

@siewers

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.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions