'Pass complex parameters to [Theory]

Xunit has a nice feature: you can create one test with a Theory attribute and put data in InlineData attributes, and xUnit will generate many tests, and test them all.

I want to have something like this, but the parameters to my method are not 'simple data' (like string, int, double), but a list of my class:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }


Solution 1:[1]

There are many xxxxData attributes in XUnit. Check out for example the MemberData attribute.

You can implement a property that returns IEnumerable<object[]>. Each object[] that this method generates will be then "unpacked" as a parameters for a single call to your [Theory] method.

See i.e. these examples from here

Here are some examples, just for a quick glance.

MemberData Example: just here at hand

public class StringTests2
{
    [Theory, MemberData(nameof(SplitCountData))]
    public void SplitCount(string input, int expectedCount)
    {
        var actualCount = input.Split(' ').Count();
        Assert.Equal(expectedCount, actualCount);
    }
 
    public static IEnumerable<object[]> SplitCountData => 
        new List<object[]>
        {
            new object[] { "xUnit", 1 },
            new object[] { "is fun", 2 },
            new object[] { "to test with", 3 }
        };
}

XUnit < 2.0: Another option is ClassData, which works the same, but allows to easily share the 'generators' between tests in different classes/namespaces, and also separates the 'data generators' from the actual test methods.

ClassData Example

public class StringTests3
{
    [Theory, ClassData(typeof(IndexOfData))]
    public void IndexOf(string input, char letter, int expected)
    {
        var actual = input.IndexOf(letter);
        Assert.Equal(expected, actual);
    }
}
 
public class IndexOfData : IEnumerable<object[]>
{
    private readonly List<object[]> _data = new List<object[]>
    {
        new object[] { "hello world", 'w', 6 },
        new object[] { "goodnight moon", 'w', -1 }
    };
 
    public IEnumerator<object[]> GetEnumerator()
    { return _data.GetEnumerator(); }
 
    IEnumerator IEnumerable.GetEnumerator()
    { return GetEnumerator(); }
}

XUnit >= 2.0: Instead of ClassData, now there's an 'overload' of [MemberData] that allows to use static members from other classes. Examples below have been updated to use it, since XUnit < 2.x is pretty ancient now. Another option is ClassData, which works the same, but allows to easily share the 'generators' between tests in different classes/namespaces, and also separates the 'data generators' from the actual test methods.

MemberData Example: look there to another type

public class StringTests3
{
    [Theory, MemberData(nameof(IndexOfData.SplitCountData), MemberType = typeof(IndexOfData))]
    public void IndexOf(string input, char letter, int expected)
    {
        var actual = input.IndexOf(letter);
        Assert.Equal(expected, actual);
    }
}
 
public class IndexOfData : IEnumerable<object[]>
{
    public static IEnumerable<object[]> SplitCountData => 
        new List<object[]>
        {
            new object[] { "hello world", 'w', 6 },
            new object[] { "goodnight moon", 'w', -1 }
        };
}

Disclaimer :)

Last time checked @20210903 with dotnetfiddle.net on C# 5.0 and xunit 2.4.1 .. and failed. I couldn't mix-in a test-runner into that fiddle. But at least it compiled fine. Note that this was originally written years ago, things changed a little. I fixed them according to my hunch and comments. So.. it may contain inobvious typos, otherwise obvious bugs that would instantly pop up at runtime, and traces of milk & nuts.

Solution 2:[2]

To update @Quetzalcoatl's answer: The attribute [PropertyData] has been superseded by [MemberData] which takes as argument the string name of any static method, field, or property that returns an IEnumerable<object[]>. (I find it particularly nice to have an iterator method that can actually calculate test cases one at a time, yielding them up as they're computed.)

Each element in the sequence returned by the enumerator is an object[] and each array must be the same length and that length must be the number of arguments to your test case (annotated with the attribute [MemberData] and each element must have the same type as the corresponding method parameter. (Or maybe they can be convertible types, I don't know.)

(See release notes for xUnit.net March 2014 and the actual patch with example code.)

Solution 3:[3]

Suppose that we have a complex Car class that has a Manufacturer class:

public class Car
{
     public int Id { get; set; }
     public long Price { get; set; }
     public Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
    public string Name { get; set; }
    public string Country { get; set; }
}

We're going to fill and pass the Car class to a Theory test.

So create a 'CarClassData' class that returns an instance of the Car class like below:

public class CarClassData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] {
                new Car
                {
                  Id=1,
                  Price=36000000,
                  Manufacturer = new Manufacturer
                  {
                    Country="country",
                    Name="name"
                  }
                }
            };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

It's time for creating a test method(CarTest) and define the car as a parameter:

[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(Car car)
{
     var output = car;
     var result = _myRepository.BuyCar(car);
}

complex type in theory

**If you're going to pass a list of car objects to Theory then change the CarClassData as follow:

public class CarClassData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] {
                new List<Car>()
                {
                new Car
                {
                  Id=1,
                  Price=36000000,
                  Manufacturer = new Manufacturer
                  {
                    Country="Iran",
                    Name="arya"
                  }
                },
                new Car
                {
                  Id=2,
                  Price=45000,
                  Manufacturer = new Manufacturer
                  {
                    Country="Torbat",
                    Name="kurosh"
                  }
                }
                }
            };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

And the theory will be:

[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(List<Car> cars)
{
   var output = cars;
}

Good Luck

Solution 4:[4]

Creating anonymous object arrays is not the easiest way to construct the data so I used this pattern in my project.

First define some reusable, shared classes:

//http://stackoverflow.com/questions/22093843
public interface ITheoryDatum
{
    object[] ToParameterArray();
}

public abstract class TheoryDatum : ITheoryDatum
{
    public abstract object[] ToParameterArray();

    public static ITheoryDatum Factory<TSystemUnderTest, TExpectedOutput>(TSystemUnderTest sut, TExpectedOutput expectedOutput, string description)
    {
        var datum= new TheoryDatum<TSystemUnderTest, TExpectedOutput>();
        datum.SystemUnderTest = sut;
        datum.Description = description;
        datum.ExpectedOutput = expectedOutput;
        return datum;
    }
}

public class TheoryDatum<TSystemUnderTest, TExpectedOutput> : TheoryDatum
{
    public TSystemUnderTest SystemUnderTest { get; set; }

    public string Description { get; set; }

    public TExpectedOutput ExpectedOutput { get; set; }

    public override object[] ToParameterArray()
    {
        var output = new object[3];
        output[0] = SystemUnderTest;
        output[1] = ExpectedOutput;
        output[2] = Description;
        return output;
    }

}

Now your individual test and member data is easier to write and cleaner...

public class IngredientTests : TestBase
{
    [Theory]
    [MemberData(nameof(IsValidData))]
    public void IsValid(Ingredient ingredient, bool expectedResult, string testDescription)
    {
        Assert.True(ingredient.IsValid == expectedResult, testDescription);
    }

    public static IEnumerable<object[]> IsValidData
    {
        get
        {
            var food = new Food();
            var quantity = new Quantity();
            var data= new List<ITheoryDatum>();
            
            data.Add(TheoryDatum.Factory(new Ingredient { Food = food }                       , false, "Quantity missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity }               , false, "Food missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity, Food = food }  , true,  "Valid" ));

            return data.ConvertAll(d => d.ToParameterArray());
        }
    }
}

The string Description property is to throw yourself a bone when one of your many test cases fail.

Solution 5:[5]

You can try this way:

public class TestClass {

    bool isSaturday(DateTime dt)
    {
       string day = dt.DayOfWeek.ToString();
       return (day == "Saturday");
    }

    [Theory]
    [MemberData("IsSaturdayIndex", MemberType = typeof(TestCase))]
    public void test(int i)
    {
       // parse test case
       var input = TestCase.IsSaturdayTestCase[i];
       DateTime dt = (DateTime)input[0];
       bool expected = (bool)input[1];

       // test
       bool result = isSaturday(dt);
       result.Should().Be(expected);
    }   
}

Create another class to hold the test data:

public class TestCase
{
   public static readonly List<object[]> IsSaturdayTestCase = new List<object[]>
   {
      new object[]{new DateTime(2016,1,23),true},
      new object[]{new DateTime(2016,1,24),false}
   };

   public static IEnumerable<object[]> IsSaturdayIndex
   {
      get
      {
         List<object[]> tmp = new List<object[]>();
            for (int i = 0; i < IsSaturdayTestCase.Count; i++)
                tmp.Add(new object[] { i });
         return tmp;
      }
   }
}

Solution 6:[6]

For my needs I just wanted to run a series of 'test users' through some tests - but [ClassData] etc. seemed overkill for what I needed (because the list of items was localized to each test).

So I did the following, with an array inside the test - indexed from the outside:

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public async Task Account_ExistingUser_CorrectPassword(int userIndex)
{
    // DIFFERENT INPUT DATA (static fake users on class)
    var user = new[]
    {
        EXISTING_USER_NO_MAPPING,
        EXISTING_USER_MAPPING_TO_DIFFERENT_EXISTING_USER,
        EXISTING_USER_MAPPING_TO_SAME_USER,
        NEW_USER

    } [userIndex];

    var response = await Analyze(new CreateOrLoginMsgIn
    {
        Username = user.Username,
        Password = user.Password
    });

    // expected result (using ExpectedObjects)
    new CreateOrLoginResult
    {
        AccessGrantedTo = user.Username

    }.ToExpectedObject().ShouldEqual(response);
}

This achieved my goal, while keeping the intent of the test clear. You just need to keep the indexes in sync but that's all.

Looks nice in the results, it's collapsable and you can rerun a specific instance if you get an error:

enter image description here

Solution 7:[7]

This is how I solved your problem, I had the same scenario. So inline with custom objects and a different number of objects on each run.

    [Theory]
    [ClassData(typeof(DeviceTelemetryTestData))]
    public async Task ProcessDeviceTelemetries_TypicalDeserialization_NoErrorAsync(params DeviceTelemetry[] expected)
    {
        // Arrange
        var timeStamp = DateTimeOffset.UtcNow;

        mockInflux.Setup(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>())).ReturnsAsync("Success");

        // Act
        var actual = await MessageProcessingTelemetry.ProcessTelemetry(JsonConvert.SerializeObject(expected), mockInflux.Object);

        // Assert
        mockInflux.Verify(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>()), Times.Once);
        Assert.Equal("Success", actual);
    }

So this is my unit test, notice the params parameter. This allow to send a different number of object. And now my DeviceTelemetryTestData class :

    public class DeviceTelemetryTestData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

Hope it helps !

Solution 8:[8]

xUnit.Sdk provides you with DataAttribute class that you could inherit and override its GetData method and use it to pass whatever you feel like you want..

I usually use it alongside DataTestBuilders pattern and build something like that..

public class ValidComplexObjectDataSource : DataAttribute
{
    public override IEnumerable<object[]> GetData(MethodInfo testMethod)
    {
        yield return new object[] {
            ComplexObjectBuilder
                .BasicComplexObject()
                .Build()
        };

        yield return new object[] {
            ComplexObjectBuilder
                .BasicComplexObject()
                .WithoutSomeAttribute()
                .Build()
        };

       // ... list all test cases you want to pass to your method
    }
}

This ComplexObjectBuilder could be whatever your object is, highly recommend checking builder pattern

[Theory]
[Trait("Validation", "CreateXYZCommand")]
[ValidComplexObjectDataSource]
public void CreateXYZCommandValidator_WithValidInput_ShouldPassAllValidations(CreateComplexObjectInput createComplexObjectInput)
{
    var command = new CreateXYZCommand(createComplexObjectInput);
    var result = _validator.TestValidate(command);
    result.ShouldNotHaveAnyValidationErrors();
}

I only demonstrated it with a single object, you have an array of objects you can yield.

yield return new object[] {
   ComplexObject_1,
   ComplexObject_2,
   string_attribute,
   int_attribute
};

and have these as arguments to your test cases.

Solution 9:[9]

You can utilize TheoryData for complex types like classes.

[Theory, MemberData(nameof(CustomClassTests))]
public async Task myTestName(MyCustomClass customClassTestData) { ... }

public record MyCustomClass { ... }

public static TheoryData<MyCustomClass> CustomClassTests {
    get {
        return new() {
            new MyCustomClass{ ... },
            new MyCustomClass{ ... },
            ...
        }; 
    }
}

Solution 10:[10]

I guess you mistaken here. What xUnit Theory attribute actually means: You want to test this function by sending special/random values as parameters that this function-under-test receives. That means that what you define as the next attribute, such as: InlineData, PropertyData, ClassData, etc.. will be the source for those parameters. That means that you should construct the source object to provide those parameters. In your case I guess you should use ClassData object as source. Also - please note that ClassData inherits from: IEnumerable<> - that means each time another set of generated parameters will be used as incoming parameters for function-under-test until IEnumerable<> produces values.

Example here: Tom DuPont .NET

Example may be incorrect - I didn't use xUnit for a long time

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Luke Girvin
Solution 3
Solution 4 Dastan Alybaev
Solution 5 Sandy_Vu
Solution 6
Solution 7 MaxThom
Solution 8 Hamza
Solution 9 thalacker
Solution 10 Ciarán Bruen