ASP.NET Core 1.0 Unit Testing

With ASP.NET Core 1.0 applications, *.csproj and *.vbproj file extensions no longer exist — instead there is the *.xproj file extension to take their place. Previously the “cs” and “vb” in the extension names were indicators of the programming language in use for that Visual Studio project (C# and Visual Basic respectively). This could be helpful I suppose, although I never really paid too much attention to the file extension of the project. You are probably wondering, “what does ‘x’ mean”? It is intended to signify that the project is a DNX project, and the language doesn’t really matter.

From within Visual Studio, let’s create a new class library project File › New › Project. This will open the “New File Project” dialog. Select Installed › Visual C# › Web › Class Library (Package). I named mine “IEvangelist.CSharper.Calculator”. As a side note, the Class Library project type is the same as the Web Site in that you can still perform xUnit testing against either. For demonstration purposes, I’m simply using a class library — the main point is that the project is a *.xproj type.

You may have noticed that the global.json file calls out a “projects” property which takes a string array of directories in which to look for projects. So let’s create the “test” directory and add another class library project under this newly created folder. I named mine “IEvangelist.CSharper.CalculatorTests”, and now your structure should be as follows:

sln-structure

This is the convention, having the src separated from the test directories. This is how the global.json looks:

{
"projects": [ "src", "test" ],
"sdk": {
"version": "1.0.0-rc1-update1"
}
}
view raw global.json hosted with ❤ by GitHub

The next important part to note, is that the calculator test project must reference the calculator proper project (obviously). This is done the same way in which it has always been done for projects that live side-by-side, simply add it as a project reference — Right click on References › Add References.... From the Reference Manager dialog, select Projects › Solution and check the “IEvangelist.CSharper.Calculator” project and click Ok.

xUnit.net

With DNX projects, I have found that they are incompatible with MSTest. However, there is a specific DNX compatible version of xUnit available — that’s what we will be using.

xUnit.net is a free, open source, community-focused unit testing tool for the .NET Framework. Written by the original inventor of NUnit v2, xUnit.net is the latest technology for unit testing C#, F#, VB.NET and other .NET languages.

Our project.json file will need to take on xUnit as a dependency as depicted here:

{
"version": "1.0.0-*",
"description": "IEvangelist.CSharper.CalculatorTests Class Library",
"authors": [ "David Pine" ],
"dependencies": {
"IEvangelist.CSharper.Calculator": "1.0.0-*",
"xunit": "2.1.0",
"xunit.runner.dnx": "2.1.0-rc1-build204"
},
"commands": {
"test": "xunit.runner.dnx"
},
"frameworks": {
"dnx451": {
"frameworkAssemblies": {
"Accessibility": "4.0.0.0"
}
},
"dnxcore50": { }
}
}

For we can focus on testing. I have created a simple interface namely, ICalculator. It exposes four simple calculation based methods.

namespace IEvangelist.CSharper.Calculator
{
public interface ICalculator<T>
{
T Add(T leftOperand, T rightOperand);
T Subtract(T leftOperand, T rightOperand);
T Multiply(T leftOperand, T rightOperand);
T Divide(T leftOperand, T rightOperand);
}
}
view raw ICalulator`1.cs hosted with ❤ by GitHub

An example implementation is the Int32Calculator defined like so. Please note that these calculator’s in all practicality are useless, these are simply for demonstration purposes only.

namespace IEvangelist.CSharper.Calculator
{
public class Int32Calculator : ICalculator<int>
{
public int Add(int leftOperand, int rightOperand) => leftOperand + rightOperand;
public int Divide(int leftOperand, int rightOperand) => leftOperand / rightOperand;
public int Multiply(int leftOperand, int rightOperand) => leftOperand * rightOperand;
public int Subtract(int leftOperand, int rightOperand) => leftOperand - rightOperand;
}
}

I love some of the new C# 6 language features, like shown above — we can have members that are defined via a lambda expression instead of a method body. In my opinion this makes certain code easier to read. Now that we have defined an implementation of the ICalculator interface — we can author a unit test against it.

By convention, I usually try creating a single class that corresponds to the system unit test — in this case, let’s create a new class in the testing project named “Int32CalculatorTests.cs”. xUnit uses two primary test attributes that are provided at the method level, Fact and Theory. These attributes are what tell xUnit to treat these methods as tests. There are several other key attributes that are available, I’ll touch on those later.

Attribute Description
Fact Expects no parameters on method definition.
Theory Expects either InlineData or ClassData attributes for parameter resolution.

Here is an example of a Fact attributed test method that tests the Int32Calculator's .Subtract method.

[Fact]
public void SubstractTest()
{
// Arrange
var calculator = new Int32Calculator();
// Act
var actual = calculator.Subtract(77, 100);
// Assert
Assert.Equal(-23, actual);
}

It is very simple and straight forward, but imagine that you wanted to test several different inputs and expected outputs — this is where Theory comes into play. The Theory attribute lets you define InlineData for example:

[Theory, InlineData(3, 3, 9), InlineData(2, 9, 18)]
public void MultiplyTest(int leftOperand, int rightOperand, int expected)
{
// Arrange
var calculator = new Int32Calculator();
// Act
var actual = calculator.Multiply(leftOperand, rightOperand);
// Assert
Assert.Equal(expected, actual);
}

The InlineData becomes a little too repetitive if you want to have more tests executing against a function. You can then use the ClassData attribute. This attribute takes a System.Type as an argument to its constructor. The type must implement the IEnumerable interface of object[]. I decided that if I was going to have lots of tests that would require this implementation for custom test data, I should create an abstract class that requires implementations to define a TestData. Here is the ClassTestData class.

using System.Collections;
using System.Collections.Generic;
namespace IEvangelist.CSharper.CalculatorTests
{
public abstract class ClassTestData : IEnumerable<object[]>
{
/// <summary>Container for known test data.</summary>
protected abstract List<object[]> TestData { get; }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<object[]> GetEnumerator() => TestData.GetEnumerator();
}
}

Here is an example unit test, it is Theory based and it takes on the Int32CalculatorTestData class — which inherits the ClassTestData class. This is the type that is passed into the Xunit.ClassDataAttribute class as an argument to its constructor.

[Theory, ClassData(typeof(Int32CalculatorTestData))]
public void AddTest(int leftOperand, int rightOperand, int expected)
{
// Arrange
var calculator = new Int32Calculator();
// Act
var actual = calculator.Add(leftOperand, rightOperand);
// Assert
Assert.Equal(expected, actual);
}
public class Int32CalculatorTestData : ClassTestData
{
protected override List<object[]> TestData => new List<object[]>
{
new object[] { 1, 7, 8 },
new object[] { 2, 2, 4 },
new object[] { -33, -77, -110 },
new object[] { 256, -512, -256 },
new object[] { 0, 0, 0 },
};
}

The amazing thing about all of this is that Visual Studio’s test explorer window resolves these at compile time and shows a parameterized test foreach InlineData or ClassData test set. Based on the three examples, here’s the Test Explorer window.

test-explorer

For those of you who are still with me, you made it through my bantering and you are now rewarded with the pointer to the source — I have posted the project up on my github here. I appreciate any and all feedback, and I hope that this was at the very least beneficial to someone!

Advertisement

2 thoughts on “ASP.NET Core 1.0 Unit Testing

Comments are closed.