Introduction
I have used a number of unit testing frameworks such as MSTest, NUnit, and MbUnit, and have found all of these to be sufficient for unit testing. I have a small personal preference XUnit, mainly because I find it a little cleaner in writing parameterized tests, and I tend to write better isolated tests with less use of attributes in XUnit.
For this tip, I will go over a few basic features while testing some dead simple code. In practice, I use fluent assertions and mocking frameworks such as Moq, but that is outside the focus here. The attached Visual Studio solution utilizes XUnit.net 2.1 and the Visual Studio runner, which allows you to debug/run the tests in Visual Studio.
The Abacus
The class to test is an Abacus, which provides simple add, subtract, multiply, and divide operations. It is a simple abacus that only works with positive numbers, and doesn't hold state. You can configure the size of your abacus simply as the largest value (ResultMax
) it can return, and the largest values (ValueMax
) it can utilize for calculations.
public class Abacus
{
public readonly int ValueMax;
public readonly int ResultMax;
public Abacus(int valueMax, int resultMax)
{
ValueMax = valueMax;
ResultMax = resultMax;
}
public int Add(int x, int y)
{
ValidateValue(x);
ValidateValue(y);
ValidateResult(x + y);
return x + y;
}
public int Subtract(int x, int y)
{
ValidateValue(x);
ValidateValue(y);
ValidateResult(x - y);
return x - y;
}
public int Multiply(int x, int y)
{
ValidateValue(x);
ValidateValue(y);
ValidateResult(x * y);
return x * y;
}
public int Divide(int x, int y)
{
ValidateValue(x);
ValidateValue(y);
ValidateResult(x / y);
return x / y;
}
void ValidateValue(int value)
{
if (value <= 0)
throw new ValidationException("Value must be greater than 0.");
if (value > ValueMax) throw new ValidationException
(String.Format("Value must be less than or equal to {0}.", ValueMax));
}
void ValidateResult(long result)
{
if (result <= 0)
throw new ValidationException("Result must be greater than 0.");
if (result > ResultMax) throw new ValidationException
(String.Format("Result must be less than or equal to {0}.", ResultMax));
}
}
This code is found in the MyLibrary
project in the attached download.
Just the Facts
The simplest way to set up an XUnit
test is to annotate a method with a Fact
attribute. A Fact
is a kind of test that is always supposed to succeed. Following are a couple of tests that test abacus add operations.
public class AbacusAddTests
{
[Fact]
public void CanAddOnePlusOne()
{
Abacus abacus = new Abacus(2, 4);
Assert.Equal(2, abacus.Add(1, 1));
}
[Fact]
public void CanAddToResultsLimit()
{
Abacus abacus = new Abacus(2, 4);
Assert.Equal(4, abacus.Add(2, 2));
}
}
The Assert.Equal
method (as opposed to Assert.AreEqual
for NUnit, etc.) is used to test the result of the test. This code for all of the tests (we are focusing only on add tests here) can be found in the XUnitTests
project in the attached download.
A Working Theory
XUnit also has a Theory
attribute, which represents a test that should succeed for certain input data. In practice, most code has a different behavior depending on inputs (such as a different result based on validation), and I find that I use Theory
to create parameterized tests much more often than Fact
. There are 3 basic ways to create Theory
based tests, and these ways will be covered below.
Theory with InlineData Attribute
The easiest way to create a Theory
based test is to use the InlineData
attribute. Our test below takes 2 parameters and adds them together and tests the result. Instead of writing 3 tests, we create 3 InlineData
attributes with different parameter values. Now we have 3 test cases with very little additional code!
[Theory]
[InlineData(2,3)]
[InlineData(4,5)]
[InlineData(5,11)]
public void CanAddNumbersFromInlineDataInput(int x, int y)
{
Abacus abacus = new Abacus(Math.Max(x, y), x + y);
int result = abacus.Add(x, y);
Assert.True(result > 0);
Assert.Equal(x + y, result);
}
I tend to use this form when the number of parameterized cases is pretty small.
Theory with MemberData Attribute
Another way to create a Theory
based test is to use the MemberData
attribute to provide the parameter information. In our add test below, the MemberData
attribute provides the AddPositiveNumberData
list to run the parameterized tests. Again, 3 different test cases are run with different parameters.
[Theory]
[MemberData("AddPositiveNumberData")]
public void CanAddNumbersFromMemberDataInput(int x, int y)
{
Abacus abacus = new Abacus(Math.Max(x, y), x + y);
int result = abacus.Add(x, y);
Assert.True(result > 0);
Assert.Equal(x + y, result);
}
private static List<object[]> AddPositiveNumberData()
{
return new List<object[]>
{
new object[] {1, 2},
new object[] {2, 2},
new object[] {5, 9}
};
}
I tend to use this for larger and/or reusable parameter data sets.
Theory with Custom DataAttribute
Finally, you can create a Theory
based test by defining and using your own custom DataAttribute
. Below, the AbacusDataAttribute
provides a means of providing an enumerable (of length Count
) of x
and y
values to be used for tests.
public class AbacusDataAttribute : DataAttribute
{
private readonly int XStart, XIncrement, YStart, YIncrement, Count;
public AbacusDataAttribute
(int xStart, int xIncrement, int yStart, int yIncrement, int count)
{
XStart = xStart;
XIncrement = xIncrement;
YStart = yStart;
YIncrement = yIncrement;
Count = count;
}
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
for (int i = 0; i < Count; i++)
{
yield return new object[]
{ XStart + i * XIncrement, YStart + i * YIncrement };
}
}
}
Here, we have another add test which uses the AbacusData
attribute to provide 20 tests cases with different x
and y
values.
[Theory]
[AbacusData(1, 2, 4, 3, 20)]
public void CanAddNumbersFromAttributeInput(int x, int y)
{
Abacus abacus = new Abacus(Math.Max(x, y), x + y);
int result = abacus.Add(x, y);
Assert.True(result > 0);
Assert.Equal(x + y, result);
}
I tend to use custom attributes if the input data can be expressed algorithmically in a useful way (this example is a little contrived).
Exceptional Tests
Using assertions in XUnit
tests is very similar to NUnit, etc., the XUnit syntax just happens to be a little more concise. XUnit takes a different approach to handling tests that throw exceptions. Instead of an ExpectedException
attribute that is more typical, XUnit has an Assert.Throws
assertion that makes it easier to manage the exception and message data right where you are performing the test actions. In our test below, we are asserting that a ValidationException
is thrown and also that the validation message is as expected.
[Theory]
[AbacusData(1, 2, 4, 5, 10)]
public void CannotOverFlowAddResult(int x, int y)
{
Abacus abacus = new Abacus(Math.Max(x, y), x);
Exception ex = Assert.Throws<ValidationException>(() => abacus.Add(x, y));
Assert.Equal(String.Format("Result must be less than or equal to {0}.", x), ex.Message);
}
There are a more test cases in the example download that you can review. Build the solution, and you should be able to run all of the tests and debug tests to your liking.
Conclusion
I hope this little walkthrough was useful for you to get started in using XUnit, and especially XUnit.net. Comment below if you would like more depth on any XUnit features.