Parameterized Test Example in .NET Core Using NUnit
A lot of times when writing unit tests we end up with a lot test methods that look the same and actually do the same thing. Read on for a better way
Join the DZone community and get the full member experience.
Join For FreeA lot of times when writing unit tests we end up with a lot test methods that look the same and actually do the same thing. Also, there are special cases where we want to have high test coverage and in-depth test access for our crucial and very important core functionality methods.
For example, when creating a framework or a library, usually we want to write many tests and cover all possible aspects and outcomes, which may lead to a large amount of certain behavior test methods.
Very often, we end up with these test methods with the same logic and behavior but with different input and data values. We are going to create parameterized tests that will test the same method but with different values.
The Scenario
Let's start with a simple method that calculates the total price of a product * quantity and then it applies the discount on the total price.
public static double calculate(double price,int quantity,double discount)
{
double totalPrice = price * quantity;
double totalPriceWithDiscount = System.Math.Round(totalPrice - (totalPrice * discount/100),2);
return totalPriceWithDiscount;
}
As simple as it looks, there is a lot of important other stuff to test here, like:
nulls
negative and zero inputs
exceptions/input validation handling
rounding
But we are not going to cover these here.
We are going to focus on the parameterized test and validating the mathematical correctness of the calculation method.
Without a parameterized test, we have this plain test.
[Test]
public void testCalculate()
{
Assert.AreEqual(100,MyClass.calculate(10,10,0));
}
This passes and, indeed, if the price of a product is 10, the quantity is 10, and we have zero discounts then the total price is 100.
The problem is that with this setup if we want to test different values and results we have to write a different test method for every different input.
The TestCase Attribute
We start by first converting the above test to a parameterized test using the TestCase attribute.
[TestCase(10,10,10,90)]
[TestCase(10,10,0,100)]
public void testCalculate(double price,int quantity,double discount,double expectedFinalAmount)
{
Assert.AreEqual(expectedFinalAmount,MyClass.calculate(price,quantity,discount));
}
Now, this method will run for every TestCase
annotation it has. A mapping will occur at runtime to the values we provided at the annotations and copied down to the method parameters. In our example, this test will run two times. We can pass reference types and value types.
Usually, the order of parameters goes by first providing the values and the last one is the expected result.
The TestCaseSource Attribute
For every different input, we have to add a TestCase
attribute at the top of the test method. To organize the code, and for reusability reasons, we are going to use the TestCaseSource attribute. We're going to create a provider method and centralize the input data.
First, we create a provider method and then move and fill it with the data we want.
public static IEnumerable<TestCaseData> priceProvider()
{
yield return new TestCaseData(10,10,10,90);
yield return new TestCaseData(10,10,0,100);
}
And also refactor the testCalculate
method to use the priceProvider
method.
[Test,TestCaseSource("priceProvider")]
public void testCalculate(double price,int quantity,double discount,double expectedFinalAmount)
{
Assert.AreEqual(expectedFinalAmount,MyClass.calculate(price,quantity,discount));
}
This is the same as having the TestCase
attributes on top of the method.
We can also provide a different class for the provider methods to isolate and centralize the code in class/file level.
[Test,TestCaseSource(typeof(MyProviderClass),"priceProvider")]
public void testCalculate(double price,int quantity,double discount,double expectedFinalAmount)
{
Assert.AreEqual(expectedFinalAmount,MyClass.calculate(price,quantity,discount));
}
priceProvider
is a static method inside MyProviderClass
.
Extra Parameterization With the Help of the TestFixture Attribute
Let's add one more step of parameterization with the help of TestFixture Attribute. Usually,TestFixture
is a class attribute to mark a class that contains tests, on the other hand, one of the biggest features is that TestFixture can take constructor arguments. NUnit will create and test a separate instance for every input set.
Let's assume that except for the final amount we test above, there is an extra amount applied depending on what category the product is, which could be category 1 or 2.
[TestFixture(typeof(int),typeof(double),1,5)]
[TestFixture(typeof(int),typeof(double),2,6.5)]
public class TestCharge<T,X>
{
T categoryType;
X extraValue;
public TestCharge(T t,X x)
{
this.categoryType = t;
this.extraValue = x;
}
}
We know, in fact, that in category one the extra amount is 5 and in category two it's 6.5.
We can now run all the tests again but also for every TestFixture
we provided.
For example, we test the calculation also depending on the category.
[Test,TestCaseSource(typeof(MyProviderClass),"priceProvider")]
public void testCalculateCategory(double price,int quantity,double discount,double expectedFinalAmount)
{
Assert.AreEqual(expectedFinalAmount+(double)(object)this.extraValue,
MyClass.calculateCategory(price,quantity,discount,(int)(object)this.categoryType));
}
Final Words
Remember, what makes a good unit test is its simplicity, the ease of reading and writing, the reliability, not to be treated as an integration test, and it has to be fast.
The original and complete repository of code samples can be found here.
Opinions expressed by DZone contributors are their own.
Comments