Pass complex parameters to [Theory]

110,504

Solution 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

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

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

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

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;
      }
   }
}
Share:
110,504
zchpit
Author by

zchpit

software developer working in Warsaw.

Updated on April 26, 2022

Comments

  • zchpit
    zchpit about 2 years

    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) { ... }
    
    • Iman Bahrampour
      Iman Bahrampour almost 5 years
      A complete guide that sends complex objects as a parameter to Test methods complex types in Unit test
    • 2nyacomputer
      2nyacomputer over 2 years
      The accepted answer passes primitive data types and not complex types to theory!! the third answer is exactly the answer.pass complex parameters in xunit
  • quetzalcoatl
    quetzalcoatl about 10 years
    @dcastro: yeah, I'm actually searching for some on original xunit docs
  • quetzalcoatl
    quetzalcoatl about 10 years
    Hm.. didn't find any. I'll use these then.
  • Nick
    Nick over 9 years
    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.
  • quetzalcoatl
    quetzalcoatl over 9 years
    @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.
  • Nick
    Nick over 9 years
    @quetzalcoatl 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.
  • Erti-Chris Eelmaa
    Erti-Chris Eelmaa about 9 years
    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.
  • Junle Li
    Junle Li almost 9 years
    @Erti, use [MemberData("{static member}", MemberType = typeof(MyClass))] to replace ClassData attribute.
  • Raymond
    Raymond over 8 years
    Some ideas for abstracting a ClassDataBase to clean the above a bit (code is in F#) stackoverflow.com/a/35127997/11635
  • sara
    sara about 8 years
    As of C#6 it'd recommended to use the nameof keyword instead of hardcoding a property name (breaks easily but silently).
  • Gustyn
    Gustyn over 7 years
    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.
  • dashesy
    dashesy almost 5 years
    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.
  • J.D. Cain
    J.D. Cain over 4 years
    This answer explicitly addresses the question of passing a custom type as the Theory input which seems to be missing from the selected answer.
  • Denis M. Kitchen
    Denis M. Kitchen over 4 years
    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!
  • Kishan Vaishnav
    Kishan Vaishnav over 4 years
    @davidbak The codplex is gone. The link is not working
  • pastacool
    pastacool about 4 years
    aren't the parameters for the IsValid Testmethod mixed up - shouldn't it be IsValid(ingrediant, exprectedResult, testDescription)?
  • Oliver Pearmain
    Oliver Pearmain almost 4 years
    "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.
  • Oliver Pearmain
    Oliver Pearmain almost 4 years
    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-108187109
  • Ash A
    Ash A over 3 years
    How can you return more than one object in the car class data?
  • srbrills
    srbrills almost 3 years
    Add multiple yield return statements with various scenarios, as many as you want, and your test will be executed that many times. andrewlock.net/…
  • Andes Lam
    Andes Lam over 2 years
    @KishanVaishnav Not much have changed imo, the only thing I changed was the attribute from PropertyData to MemberData
  • Iman Bahrampour
    Iman Bahrampour over 2 years
    @AshA. Sorry I saw the comment late. post edited
  • Paul Farry
    Paul Farry almost 2 years
    @KishanVaishnav updated link for Patch :-)