Pass complex parameters to [Theory]
Asked Answered
J

13

157

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) { ... }
Jaf answered 28/2, 2014 at 11:25 Comment(2)
A complete guide that sends complex objects as a parameter to Test methods complex types in Unit testMckenney
The accepted answer passes primitive data types and not complex types to theory!! the third answer is exactly the answer.pass complex parameters in xunitIllsuited
P
196

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.

Prodigal answered 28/2, 2014 at 11:31 Comment(10)
@dcastro: yeah, I'm actually searching for some on original xunit docsProdigal
Hm.. didn't find any. I'll use these then.Prodigal
When or why would you use ever ClassData? It looks like just a heavier version of PropertyData. Instead of making that whole IndexOfData class, I would much rather just turn that _data field into a static property and use it in a PropertyData.Willenewillet
@Nick: I agree that's similar to PropertyData, but also, you have pointed out the reason for it: static. That's exactly why I wouldn't. ClassData is when you want to escape from statics. By doing so, you can reuse (i.e. nest) the generators easier.Prodigal
@Prodigal Oh, I see. You might have multiple data sources that have some things in common so you'd only have to write that once in a base class and the others could inherit it.Willenewillet
Any ideas what happened with ClassData? I canõt find it in xUnit2.0, for now, I am using MemberData with static method, which creates new instance of class, and returns that.Increasing
@Erti, use [MemberData("{static member}", MemberType = typeof(MyClass))] to replace ClassData attribute.Vidavidal
Some ideas for abstracting a ClassDataBase to clean the above a bit (code is in F#) https://mcmap.net/q/152735/-in-f-how-do-you-pass-a-collection-to-xunit-39-s-inlinedata-attributeEnlist
As of C#6 it'd recommended to use the nameof keyword instead of hardcoding a property name (breaks easily but silently).Ecru
ClassData cannot be easily re-used because there is no way to instantiate it differently for each test. MemberData however can easily pass MemberType and use the same class for different tests.Ramses
M
60

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

Mckenney answered 2/6, 2019 at 8:10 Comment(5)
This answer explicitly addresses the question of passing a custom type as the Theory input which seems to be missing from the selected answer.Gisborne
This is exactly the use-case I was looking for which is how to pass a complex type as a parameter to a Theory. Works perfectly! This really pays off for testing MVP patterns. I can now setup many different instances of a View in all sorts of states and pass them all into the same Theory which tests the effects that Presenter methods have on that view. LOVE it!Usquebaugh
How can you return more than one object in the car class data?Triviality
Add multiple yield return statements with various scenarios, as many as you want, and your test will be executed that many times. andrewlock.net/…Packaging
@AshA. Sorry I saw the comment late. post editedMckenney
K
49

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.)

Kiddy answered 6/4, 2015 at 7:48 Comment(3)
@Kiddy The codplex is gone. The link is not workingMince
@KishanVaishnav Not much have changed imo, the only thing I changed was the attribute from PropertyData to MemberDataMaiden
@KishanVaishnav updated link for Patch :-)Adversative
A
11

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.

Alben answered 15/4, 2016 at 5:8 Comment(2)
I like this; it has some real potential for a very complex object I have to validate the validations on 90+ properties. I can pass in a simple JSON object, deserialize it, and generate the data for a test iteration. Good job.Brozak
aren't the parameters for the IsValid Testmethod mixed up - shouldn't it be IsValid(ingrediant, exprectedResult, testDescription)?Ivanivana
D
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

Dm answered 29/12, 2018 at 20:39 Comment(2)
"Looks nice in the results, it's collapsable and you can rerun a specific instance if you get an error". Very good point. A major drawback of MemberData seems to be that you cannot see nor run the test with a specific test input. It sucks.Fertility
Actually, I've just worked out that it is possible with MemberData if you use TheoryData and optionally IXunitSerializable. More info and exmaples here... github.com/xunit/xunit/issues/429#issuecomment-108187109Fertility
T
4

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;
      }
   }
}
Thiele answered 27/1, 2016 at 19:57 Comment(0)
Y
1

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 !

Yesteryear answered 6/3, 2020 at 14:9 Comment(0)
G
1

Despite this has already been answered I just want to add an improvement here.

The restriction of passing objects in InlineData attribute is not a limitiation of xUnit itself but C# attributes.

See this compiler error: Compiler Error CS0182

Gobbet answered 13/1, 2023 at 2:28 Comment(0)
E
1

Here is my solution to the problem.

https://github.com/xunit/xunit/issues/2760

The advantage is that it hides the yields and enumerators from the user code.

Introduce a new attribute and an interface.

public class InlineObjectDataAttribute<T> : ClassDataAttribute where T : IInlineObjects
{
    public InlineObjectDataAttribute() : base(typeof(GenericTestData)) { }

    class GenericTestData : IEnumerable<object?[]>
    {
        public IEnumerator<object?[]> GetEnumerator()
        {
            foreach (var item in T.GetObjects())
            {
                yield return item;
            }
        }

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

public interface IInlineObjects
{
    static abstract IEnumerable<object?[]> GetObjects();
    static object?[] Line(params object?[] data) => data;
}

In your test class use the following:

using static IInlineObjects;

class MyTestClass : IInlineObjects
{
    public static IEnumerable<object?[]> GetObjects() => new object?[][]
    {
        Line(180d, new DateTime(2000, 1, 1, 6, 0, 0)),
    };
}

[Theory]
[InlineObjectData<MyTestClass>]
public void TestMethod(object o1, object o2)
Eolith answered 5/8, 2023 at 15:48 Comment(0)
M
0

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.

Monochromatism answered 24/3, 2022 at 10:28 Comment(0)
K
0

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{ ... },
            ...
        }; 
    }
}
Kamerun answered 25/4, 2022 at 16:48 Comment(0)
D
0

Here is my way of defining complex parameters as TheoryData.

  1. Define Generic Param Structure
public struct ExpectedValueTestData<TParameters, TExpected>
{
    public string Name;
    public TParameters Params;
    public TExpected ExpectedValue;

    public override string ToString()
    {
        return $"{this.Name}";
    }
}
  1. Prepare complex data using Theory data
public class ValidValueTests : TheoryData<ExpectedValueTestData<Parameters, BoolMessage>>
        {
            private readonly Fixture _fixture;
            public ValidValueTests()
            {
                _fixture = new Fixture();
                this.Add(new ExpectedValueTestData<Parameters, BoolMessage>
                {
                    Name = @"Event Name - valid call for create",
                    Params = new Parameters
                    {
                        request = _fixture.Build<EventData>()
                                                    .With(data => data.Id, 1001)
                                                    .With(data => data.Operation, Operation.Create)
                                                    .Create(),
                        context = null
                    },
                    ExpectedValue = new BoolMessage { Status = true },
                });
                this.Add(new ExpectedValueTestData<Parameters, BoolMessage>
                {
                    Name = @"Event Name - valid call for update",
                    Params = new Parameters
                    {
                        request = _fixture.Build<EventData>()
                                                    .With(data => data.Id, 1001)
                                                    .With(data => data.Operation, Operation.Update)
                                                    .OmitAutoProperties()
                                                    .Create(),
                        context = null
                    },
                    ExpectedValue = new BoolMessage { Status = true },
                });
            }
        }

refer this link for more details - https://medium.com/@sanjaysoni_48818/xunit2-automoq-autofixture-trilogy-44ee8598f281

Good Luck

Diehard answered 16/11, 2023 at 16:7 Comment(0)
T
-1

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

Trireme answered 28/2, 2014 at 11:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.