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:
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" | |
} | |
} |
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); | |
} | |
} |
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.
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!
Good job David, thanks..
LikeLike
@Christos, thank you! It is an honor to have you commenting on my post – you have been an inspiration to me and for that I thank you.
LikeLike