Table of Contents

Generating custom theory data

The parameter attributes in this package cover common cases, but ordinary xUnit member data is more expressive. CombinatorialTestCaseGenerator and CombinatorialTheoryDataBuilder expose the same generation capabilities for use in your own fields, properties, and methods.

Mix hand-authored rows with generated columns

Use AddRows(params ReadOnlySpan<object?[]>) to establish a table of correlated base values. Each call to AddValues<T>(params ReadOnlySpan<T>) adds one generated column. Passing an array to AddValues treats each array element as a candidate value for that column.

public static IReadOnlyCollection<ITheoryDataRow> Cases =>
    new CombinatorialTheoryDataBuilder()
        .AddRows([10, 0], [5, 2])
        .AddValues(true, false)
        .AddTestCase(6, 2, false)
        .BuildCombinations();

[Theory, MemberData(nameof(Cases))]
public void Example(int a, int b, bool c)
{
    Assert.True(a > b);
}

The base table contains the (10, 0) and (5, 2) rows. The Boolean candidates are spread across both rows, producing four combinations. AddTestCase(params ReadOnlySpan<object?>) appends the bespoke (6, 2, false) case without expanding it.

Call AddRows before the first AddValues call. Additional AddRows calls append complete rows with the same width as the original base table. Explicit test cases must match the final width after all generated columns are added.

Constrain generated rows

Where(CombinatorialTheoryDataPredicate) accepts a predicate over the completed row. Constraints are applied during generation, so pairwise generation can seek other rows that retain as much pair coverage as possible. Explicit rows added with AddTestCase are not constrained.

public static IReadOnlyCollection<ITheoryDataRow> CreateCases(int? seed = null)
{
    return new CombinatorialTheoryDataBuilder()
        .AddValues(0, 1, 2)
        .AddValues("small", "large")
        .AddValues(false, true)
        .Where(row => !Equals(row[0], 0) || !Equals(row[1], "large"))
        .BuildPairwiseCombinations(seed);
}

BuildCombinations returns the exhaustive Cartesian product. BuildPairwiseCombinations returns a smaller covering set.

Reproduce or vary pairwise results

Pairwise generation is deterministic when its optional seed is omitted. Pass a stable integer seed to reproduce another covering set:

IReadOnlyCollection<ITheoryDataRow> rows = ConstrainedPairwiseData.CreateCases(seed: 42);

To vary coverage between runs, generate a seed and record it with the test output so a failure can be reproduced:

int seed = Random.Shared.Next();
IReadOnlyCollection<ITheoryDataRow> rows = ConstrainedPairwiseData.CreateCases(seed);

Seeds apply only to pairwise generation. Exhaustive generation always returns the complete set in stable order.

Generate permutations

GeneratePermutations<T>(ReadOnlySpan<T>) returns every positional permutation of its input:

public static IEnumerable<object?[]> Permutations =>
    CombinatorialTestCaseGenerator.GeneratePermutations([1, 2, 3])
        .Select(permutation => new object?[] { permutation });

[Theory, MemberData(nameof(Permutations))]
public void Example(int[] values)
{
    Assert.Equal(3, values.Length);
}

Input positions are distinct. If the input contains equal values, equal output rows may therefore appear.

Work directly with dimension indices

For complete control, use GenerateCombinations(ReadOnlySpan<int>, CombinatorialIndexPredicate?) or GeneratePairwiseCombinations(ReadOnlySpan<int>, CombinatorialIndexPredicate?, int?). Each result contains one selected zero-based candidate index per dimension.

public static IEnumerable<object?[]> Cases
{
    get
    {
        string[] operatingSystems = ["Windows", "Linux"];
        int[] runtimes = [8, 9, 10];
        foreach (int[] selection in CombinatorialTestCaseGenerator.GeneratePairwiseCombinations(
            [operatingSystems.Length, runtimes.Length]))
        {
            yield return [operatingSystems[selection[0]], runtimes[selection[1]]];
        }
    }
}

Both methods accept an optional CombinatorialIndexPredicate that can reject index selections. Use the higher-level builder when you want constraints to inspect actual theory argument values instead.