Introduction
In this article we will consider the different ways of testing a class.
The environment used is :
- NUnit 2.6.2
- Resharper to run the tests
Tested Code
public sealed class Calculator : ICalculator
{
public int Divide(int a, int b)
{
return a/b;
}
}
During this project we will test our calculator for three test cases:
- 1/1 = 1
- 2/1 = 2
- 1/0 is not valid
Naïve solution
[TestFixture]
public sealed class CalculatorTest1
{
private Calculator _calculator;
[SetUp]
public void Setup()
{
_calculator = new Calculator();
}
[Test]
public void OneDividedByOne()
{
int result = _calculator.Divide(1, 1);
Assert.AreEqual(1, result);
}
[Test]
public void TwoDividedByOne()
{
int result = _calculator.Divide(2, 1);
Assert.AreEqual(2, result);
}
[Test]
public void OneDividedByZero()
{
Assert.Throws<DivideByZeroException>(() => _calculator.Divide(1, 0));
}
}
Here is the naïve solution you may have coded before reading NUnit documentation.
Let's try to make it a bit better.
[TestFixture]
public sealed class CalculatorTest2
{
[SetUp]
public void Setup()
{
_calculator = new Calculator();
}
private Calculator _calculator;
[TestCase(1, 1, ExpectedResult = 1, TestName = "OneDividedByOne")]
[TestCase(2, 1, ExpectedResult = 2, TestName = "TwoDividedByOne")]
[TestCase(1, 0, ExpectedResult = 0, ExpectedException = typeof (DivideByZeroException), TestName = "OneDividedByZero")]
public int CalculatorTestMethod(int firstNumber, int secondNumer)
{
return _calculator.Divide(firstNumber, secondNumer);
}
}
The use of the TestCase attribute make the code shorter grouping all the cases in one method.
This way of coding tests seems to be really adapted to test our Calculator
.
Adding a new TestCase
is really quick and clear.
However if your tested method takes reference types as parameters which is not a string, you can't use the TestCase
attribute.
[TestFixture]
public sealed class CalculatorTest3
{
private Calculator _calculator;
[SetUp]
public void Setup()
{
_calculator = new Calculator();
}
[TestCaseSource(typeof(CalculatorTest3TestCaseDataFactory), "TestCases")]
public void CalculatorTestMethod(int firstNumber, int secondNumer, int expectedResult)
{
var result = _calculator.Divide(firstNumber, secondNumer);
Assert.AreEqual(expectedResult, result,"A meaning description to help if test crashes");
}
}
With the associated test case data factory
public class CalculatorTest3TestCaseDataFactory
{
public static IEnumerable TestCases
{
get
{
yield return new TestCaseData(1, 1, 1).SetName("OneDividedByOne");
yield return new TestCaseData(2, 1, 2).SetName("TwoDividedByOne");
yield return new TestCaseData(1, 0, default(int))
.Throws(typeof(DivideByZeroException))
.SetName("OneDividedByZero");
}
}
}
This implementation allows to divide the concern :
CalculatorTest3
containing the way to test data
CalculatorTest3TestCaseDataFactory
providing the data
In our trivial example we must have used the Returns()
method to set our expected result.
Let me tell you why I won't use that in the case my tested class is returning a reference type MyClass
I coded :
- NUnit will use the
MyClass.Equals
and maybe I want to compare my objects differently
- I can add meaning descriptions coding my own comparison in the
TestClass
Typing our data
Let's declare CalculatorTestCaseData
that will hold our typed data
public class CalculatorTestCaseData
{
public int FirstNumber { get; set; }
public int SecondNumber { get; set; }
public int ExpectedResult { get; set; }
}
Now the Test Class will be
[TestFixture]
public sealed class CalculatorTest4
{
private Calculator _calculator;
[SetUp]
public void Setup()
{
_calculator = new Calculator();
}
[TestCaseSource(typeof(CalculatorTestCaseDataFactory4), "GetTestCases")]
public void CalculatorTestMethod(CalculatorTestCaseData testCase)
{
int result = _calculator.Divide(testCase.FirstNumber, testCase.SecondNumber);
Assert.AreEqual(testCase.ExpectedResult, result);
}
}
And the factory that will instanciate CalculatorTestCaseData
public class CalculatorTestCaseDataFactory4
{
public IEnumerable GetTestCases
{
get
{
yield return new TestCaseData(OneDividedByOne()).SetName("OneDividedByOne");
yield return new TestCaseData(TwoDividedByOne()).SetName("TwoDividedByOne");
yield return new TestCaseData(OneDividedByZero())
.Throws(typeof(DivideByZeroException))
.SetName("OneDividedByZero");
}
}
private CalculatorTestCaseData OneDividedByOne()
{
return new CalculatorTestCaseData
{
FirstNumber = 1,
SecondNumber = 1,
ExpectedResult = 1
};
}
private CalculatorTestCaseData TwoDividedByOne()
{
return new CalculatorTestCaseData
{
FirstNumber = 2,
SecondNumber = 1,
ExpectedResult = 2
};
}
private CalculatorTestCaseData OneDividedByZero()
{
return new CalculatorTestCaseData
{
FirstNumber = 1,
SecondNumber = 0
};
}
}
We are now manipulating typed data, the test cases are much more meaningful than in previous example.
One drawback in this example is that test case names and method names are the same, unfortunately it's not refactoring proof (you got to change each one if you want to change your names).
Let's fix it with the next example.
Make it refactoring proof
[TestFixture]
public sealed class CalculatorTest5
{
private Calculator _calculator;
[SetUp]
public void Setup()
{
_calculator = new Calculator();
}
[TestCaseSource(typeof(CalculatorTestCaseDataFactory5), "GetTestCases")]
public void CalculatorTestMethod(CalculatorTestCaseData testCase)
{
int result = _calculator.Divide(testCase.FirstNumber, testCase.SecondNumber);
Assert.AreEqual(testCase.ExpectedResult, result);
}
}
With the data factory
public class CalculatorTestCaseDataFactory5
{
private static readonly TestCaseDataFactory<CalculatorTestCaseData> TestCaseDataFactory = new TestCaseDataFactory<CalculatorTestCaseData>();
public IEnumerable GetTestCases
{
get
{
yield return OneDividedByOne();
yield return TwoDividedByOne();
yield return OneDividedByZero().Throws(typeof(DivideByZeroException));
}
}
private TestCaseData OneDividedByOne()
{
var calculatorTCD = new CalculatorTestCaseData
{
FirstNumber = 1,
SecondNumber = 1,
ExpectedResult = 1
};
return TestCaseDataFactory.Get(calculatorTCD);
}
private TestCaseData TwoDividedByOne()
{
var calculatorTCD = new CalculatorTestCaseData
{
FirstNumber = 2,
SecondNumber = 1,
ExpectedResult = 2
};
return TestCaseDataFactory.Get(calculatorTCD);
}
private TestCaseData OneDividedByZero()
{
var calculatorTCD = new CalculatorTestCaseData
{
FirstNumber = 1,
SecondNumber = 0
};
return TestCaseDataFactory.Get(calculatorTCD);
}
}
And the TestCaseDataFactory
public sealed class TestCaseDataFactory<T>
{
public TestCaseData Get(T data, [CallerMemberName] string memberName = "noName")
{
return new TestCaseData(data).SetName(memberName);
}
}
We got all the advantages of previous version with no repetition for names. You will find attached a Resharper template for making this version of testing.
Passing arguments through TestFixture
Now let's imagine we got another implementation of our ICalculator
that is working exactly the same for same inputs : the SlowCalculator
. We will have to test it using the same test cases (the ones of the previous part). A clean way of doing it is passing arguments to the TestFixture
attribute as follow.
[TestFixture(CalculatorType.Standard)]
[TestFixture(CalculatorType.Slow)]
public sealed class CalculatorTest6
{
private ICalculator _calculator;
private readonly CalculatorType _calculatorType;
private readonly CalculatorFactory _calculatorFactory;
public CalculatorTest6(CalculatorType calculatorType)
{
_calculatorType = calculatorType;
_calculatorFactory = new CalculatorFactory();
}
[SetUp]
public void Setup()
{
_calculator = _calculatorFactory.Get(_calculatorType);
}
[TestCaseSource(typeof(CalculatorTestCaseDataFactory5), "GetTestCases")]
public void CalculatorTestMethod(CalculatorTestCaseData testCase)
{
int result = _calculator.Divide(testCase.FirstNumber, testCase.SecondNumber);
Assert.AreEqual(testCase.ExpectedResult, result);
}
}
Here is the CalculatorFactory
public class CalculatorFactory
{
public ICalculator Get(CalculatorType calculatorType)
{
switch (calculatorType)
{
case CalculatorType.Standard: return new Calculator();
case CalculatorType.Slow: return new SlowCalculator();
}
string message = String.Format("No implementation matching for type {0}. Please add it", calculatorType);
throw new ArgumentException(message);
}
}
The three test cases will be applied to the normal Calculator
and then to our new SlowCalculator
Output of our tests execution
Conclusion
We have seen various versions for coding unit tests with NUnit.
Regarding the class you have to test you will have to choose the appropriate way of doing. With the Calculator
example the one with TestCase attribute seems to be efficient and much simple.
In harder examples, TestCaseSource could help you to get an adapted code coverage.
So now you got no more excuses to skip coding tests, GO GO GO.