C# – Parameterized tests in xUnit

Here’s an example of adding a parameterized unit test in xUnit:

[Theory] [InlineData(0, 0, true, 0.0)] [InlineData(0, 1.5, false, 18.0)] [InlineData(0, 4, false, 24.0)] public void GetSpeedNorwegianBlueParrot(int numberOfCoconuts, double voltage, bool isNailed, double expectedSpeed) { //arrange var parrot = new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, numberOfCoconuts, voltage, isNailed); //act var actualSpeed = parrot.GetSpeed(); //assert Assert.Equal(expectedSpeed, actualSpeed); }
Code language: C# (cs)

To parameterize a unit test, you have to do three things:

  • Add the [Theory] attribute.
  • Add the parameters to the unit test method. In the example above, there are four parameters.
  • Add one [InlineData] for each combo of data you want to test.

If you’re used to MSUnit, [Theory] is equivalent [DataMethod], and [InlineData] is equivalent to [DataRow].

In the rest of the article, I will show how to add parameterized tests with dynamic data and explain when you should use parameterized tests.

Parameterized tests with dynamic data

Here’s how to add parameterized unit test and pass in dynamic data:

[Theory] [MemberData(nameof(NorwegianBlueParrotTestData))] public void GetSpeedNorwegianBlueParrot(Parrot parrot, double expectedSpeed) { //act var actualSpeed = parrot.GetSpeed(); //assert Assert.Equal(expectedSpeed, actualSpeed); } public static IEnumerable<object[]> NorwegianBlueParrotTestData() { yield return new object[] { new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, 0, 0, true), 0.0 }; yield return new object[] { new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, 0, 1.5, false), 18.0 }; yield return new object[] { new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, 0, 4, false), 24.0 }; }
Code language: C# (cs)

There are four steps to adding a parameterized unit test with dynamic data:

  • Add the [Theory] attribute.
  • Add the parameters to the unit test method.
  • Add a method that returns the dynamic data by yielding object[]’s. Note: This is a generator method.
  • Add the [MemberData] attribute, specifying the name of the generator method.

If you’re used to MSUnit, [MemberData] is equivalent to [DynamicData].

This is much more complicated than passing in constant data. The key problem is that attributes only accept constant values. When you’re passing in dynamic data and using the [MemberData] attribute, you’re passing in the name of a method, which is a constant. This is a pretty clever way that unit test frameworks have used to support parameterized unit tests with dynamic data.

I wouldn’t recommend using this approach too often. Think of unit tests as being defined by three sections: arrange, act, and assert. The problem with this dynamic data approach is it splits the arrange section into multiple methods (the generator method and the unit test method). And secondly, when a dynamic data unit tests fail, they are much harder to troubleshoot. Note: One trick around that problem is to pass in a string parameter that says the test case name.

When to add parameterized tests

Consider the following three individual unit tests:

[Fact] public void GetSpeedNorwegianBlueParrot_nailed() { var parrot = new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, 0, 0, true); Assert.Equal(0.0, parrot.GetSpeed()); } [Fact] public void GetSpeedNorwegianBlueParrot_not_nailed() { var parrot = new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, 0, 1.5, false); Assert.Equal(18.0, parrot.GetSpeed()); } [Fact] public void GetSpeedNorwegianBlueParrot_not_nailed_high_voltage() { var parrot = new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, 0, 4, false); Assert.Equal(24.0, parrot.GetSpeed()); }
Code language: C# (cs)

Note: This code is from Parrot Refactoring Kata.

They are all constructing a Parrot object and asserting the output of the GetSpeed() method. Notice that the only difference in these tests is the data they are using.

When tests are nearly identical, and the only difference is the data they are using, then you can get rid of all the individual tests and combine them into one parameterized test.

If the tests vary for other reasons besides the data, then don’t parameterize them. If a parameterized test needs conditional logic based on the parameters, then it’s better to have this split into individual tests.

Leave a Comment