Click here to Skip to main content
15,886,864 members
Articles / DevOps / Unit Testing
Tip/Trick

Doing Some Unit Testing with XUnit

Rate me:
Please Sign up or sign in to vote.
4.80/5 (5 votes)
4 Feb 2017CPOL4 min read 22.9K   137   9   2
Using XUnit.net to develop some unit tests, using Fact and Theory form of testing, including happy path tests and tests expected to throw exceptions

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.

C#
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.

C#
public class AbacusAddTests
{
    [Fact]
    public void CanAddOnePlusOne()
    {
        // arrange
        Abacus abacus = new Abacus(2, 4);

        // act/assert
        Assert.Equal(2, abacus.Add(1, 1));
    }

    [Fact]
    public void CanAddToResultsLimit()
    {
        // arrange
        Abacus abacus = new Abacus(2, 4);

        // act/assert
        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!

C#
[Theory]
[InlineData(2,3)]
[InlineData(4,5)]
[InlineData(5,11)]
public void CanAddNumbersFromInlineDataInput(int x, int y)
{
    // arrange
    Abacus abacus = new Abacus(Math.Max(x, y), x + y);

    // act
    int result = abacus.Add(x, y);

    // assert
    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.

C#
[Theory]
[MemberData("AddPositiveNumberData")]
public void CanAddNumbersFromMemberDataInput(int x, int y)
{
    // arrange
    Abacus abacus = new Abacus(Math.Max(x, y), x + y);

    // act
    int result = abacus.Add(x, y);

    // assert
    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.

C#
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.

C#
[Theory]
[AbacusData(1, 2, 4, 3, 20)]
public void CanAddNumbersFromAttributeInput(int x, int y)
{
    // arrange
    Abacus abacus = new Abacus(Math.Max(x, y), x + y);

    // act
    int result = abacus.Add(x, y);

    // assert
    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.

C#
[Theory]
[AbacusData(1, 2, 4, 5, 10)]
public void CannotOverFlowAddResult(int x, int y)
{
    // arrange
    Abacus abacus = new Abacus(Math.Max(x, y), x);

    // act/assert
    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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Intelligent Coding Solutions, LLC
United States United States
I enjoy coding like an excellent beer. My particular passion and experience lies in the realm of modeling and code generation. In the late 80s and early 90s, I was involved in early modeling and code generation tools that reached the marketplace, including a tool that modeled FORTRAN programs and generated FORTRAN for parallel supercomputer architectures, and a tool that managed Shlaer-Mellor models and generated C++ code. Over the years, I have applied Shlaer-Mellor, UML, and custom modeling and various code generation techniques to greatly benefit the development of enterprise applications.

My current passion and endeavor is to foster the evolution of model oriented development. In particular, I am passionate about evolving the Mo+ model oriented programming language and related model oriented development tools, with as much community input as possible to achieve higher levels in the ability to create and maintain code. The open source site is at https://github.com/moplus/modelorientedplus.

Comments and Discussions

 
QuestionSeems a little out of date Pin
John Brett31-Jan-17 22:19
John Brett31-Jan-17 22:19 
AnswerRe: Seems a little out of date Pin
Dave Clemmer1-Feb-17 13:34
Dave Clemmer1-Feb-17 13:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.