Pass complex parameters to [Theory]
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);
}
**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;
}
}
}
Comments
-
zchpit about 2 years
Xunit has a nice feature: you can create one test with a
Theory
attribute and put data inInlineData
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 almost 5 yearsA complete guide that sends complex objects as a parameter to Test methods complex types in Unit test
-
2nyacomputer over 2 yearsThe 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 about 10 years@dcastro: yeah, I'm actually searching for some on original xunit docs
-
quetzalcoatl about 10 yearsHm.. didn't find any. I'll use these then.
-
Nick over 9 yearsWhen 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 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 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 about 9 yearsAny 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 almost 9 years@Erti, use
[MemberData("{static member}", MemberType = typeof(MyClass))]
to replaceClassData
attribute. -
Raymond over 8 yearsSome ideas for abstracting a
ClassDataBase
to clean the above a bit (code is in F#) stackoverflow.com/a/35127997/11635 -
sara about 8 yearsAs of C#6 it'd recommended to use the
nameof
keyword instead of hardcoding a property name (breaks easily but silently). -
Gustyn over 7 yearsI 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 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 passMemberType
and use the same class for different tests. -
J.D. Cain over 4 yearsThis 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 over 4 yearsThis 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 over 4 years@davidbak The codplex is gone. The link is not working
-
pastacool about 4 yearsaren't the parameters for the IsValid Testmethod mixed up - shouldn't it be IsValid(ingrediant, exprectedResult, testDescription)?
-
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 almost 4 yearsActually, I've just worked out that it is possible with
MemberData
if you useTheoryData
and optionallyIXunitSerializable
. More info and exmaples here... github.com/xunit/xunit/issues/429#issuecomment-108187109 -
Ash A over 3 yearsHow can you return more than one object in the car class data?
-
srbrills almost 3 yearsAdd 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 over 2 years@KishanVaishnav Not much have changed imo, the only thing I changed was the attribute from PropertyData to MemberData
-
Iman Bahrampour over 2 years@AshA. Sorry I saw the comment late. post edited
-
Paul Farry almost 2 years@KishanVaishnav updated link for Patch :-)