Introduction
A test project contains unit tests. The unit tests contain test methods. How many test methods should a unit test contain and how should
they be named to still behave like a control over the whole testing process? This article shows, that with only one
TestMethod1()
you can produce unit tests covering multiple test cases and you
won't lose the grip.
First I'll show you how to create test cases for simple scenarios - using only constant values. Further you'll see creating test cases for more complicated objects containing references to other objects or services.
Background
Instead of creating multiple test methods, e.g.,
TestDivisionIfDivisorIs0()
, TestDivisionIfDivisorIs1()
, TestDivisionIfDivisorIsBiggerThanDividend()
, etc.,
you create TestCases defining the test environment and provide them to the test method one after another. The test method handles the test cases in a loop.
Simple scenario -
Using constant values
The Calculator.Divide()
method should be tested.
public class Calculator
{
public int Divide(int dividend, int divisor)
{
return dividend/divisor;
}
}
First we create a test project and add a UnitTest1
to it. TestMethod1()
is created automatically.
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
}
}
In the next step we define a test case as a private class of UnitTest1
.
[TestClass]
public class UnitTest1
{
(...)
private class TestCase
{
public TestCase(int dividend, int divisor,
int expectedResult, Type expectedExceptionType)
{
Dividend = dividend;
Divisor = divisor;
ExpectedResult = expectedResult;
ExpectedExceptionType = expectedExceptionType;
}
public int Dividend { get; set; }
public int Divisor { get; set; }
public int ExpectedResult { get; set; }
public Type ExpectedExceptionType { get; set; }
public string Description { get; set; }
}
}
The TestCase
class contains input parameters of the Divide()
method, the expected result, and the exception type if any exception should be thrown
by the tested method. The additional property Description
informs about the test conditions in a human readable way and helps us in identifying the test case.
Further we create a method generating test cases.
private IEnumerable<TestCase> getTestCases()
{
var tc = new TestCase(0, 0, 0, typeof (DivideByZeroException));
tc.Description = "both Dividend and Diviser are 0";
yield return tc;
}
and finally we implement TestMethod1()
.
[TestMethod]
public void TestMethod1()
{
foreach (var testCase in getTestCases())
{
var c = new Calculator();
try
{
var result = c.Divide(testCase.Dividend, testCase.Divisor);
Assert.AreEqual(testCase.ExpectedResult, result);
Assert.IsNull(testCase.ExpectedExceptionType);
}
catch (Exception ex)
{
Assert.IsNotNull(testCase.ExpectedExceptionType);
Assert.AreEqual(testCase.ExpectedExceptionType, ex.GetType());
}
}
}
TestMethod1()
takes test cases from getTestCases()
one after another and for each test case it initializes the test environment,
then the tested class is created, the tested method is invoked, and the expected result is compared with the computed one.
Providing new test cases is very easy by extending the getTestCases()
method with new items.
private IEnumerable<TestCase> getTestCases()
{
(...)
tc = new TestCase(0, 1, 0, null);
tc.Description = "Dividend is 0, Diviser is > 0";
yield return tc;
tc = new TestCase(1, 0, 0, typeof(DivideByZeroException));
tc.Description = "Dividend is > 0, Diviser is 0";
yield return tc;
}
In the above way we provide all test cases from a single place describing them in a human readable way which helps us
in finding a failed test case during debugging.
Advanced scenario - using Mocks
The above example is simple. There are only constant values provided to the test case. Imagine a method making computations based on current time. At first replacing DateTime.Now
with ITimeProvider
would be necessary to make tests available.
public class TimeCalculator
{
private readonly ITimeProvider _timeProvider;
public TimeCalculator(ITimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public bool IsTodayMonday()
{
return _timeProvider.Now.DayOfWeek == DayOfWeek.Monday;
}
}
ITimeProvider
provides the current time.
public interface ITimeProvider
{
DateTime Now { get; }
}
In production we use an instance of SystemTimeProvider
.
public class SystemTimeProvider : ITimeProvider
{
public DateTime Now
{
get { return DateTime.Now; }
}
}
But to prepare a test case we create a mock - ConstantTimeProvider
. This way we can use the same time for every method invocation.
public class ConstantTimeProvider : ITimeProvider
{
public ConstantTimeProvider(DateTime now)
{
Now = now;
}
public DateTime Now { get; set; }
}
A sample TestCase
would be as follows. For simplicity the expected exception was removed.
private class TestCase
{
public TestCase(ITimeProvider timeProvider, bool expectedResult,
string description)
{
TimeProvider = timeProvider;
ExpectedResult = expectedResult;
Description = description;
}
public ITimeProvider TimeProvider { get; set; }
public bool ExpectedResult { get; set; }
public string Description { get; set; }
}
Content of the getTestCases()
method.
private IEnumerable<TestCase> getTestCases()
{
var timeProvider = new ConstantTimeProvider(new DateTime(2013, 01, 06));
var tc = new TestCase(timeProvider, false, "today is sunday");
yield return tc;
timeProvider = new ConstantTimeProvider(new DateTime(2013, 01, 07));
tc = new TestCase(timeProvider, true, "today is monday");
yield return tc;
}
And the content of the TestMethod1()
.
public void TestMethod1()
{
foreach (var tc in getTestCases())
{
var calculator = new TimeCalculator(tc.TimeProvider);
var result = calculator.IsTodayMonday();
Assert.AreEqual(tc.ExpectedResult, result,
string.Format("Expected {0}, {1}", tc.ExpectedResult, tc.Description));
}
}
Summary
Some say (thanks for the feedback), this approach could be replaced using NUnit and its TestCaseAttribute. Indeed this could be done in the simple scenario - divide example - but the advanced one -using ITimeProvider
mock - needs creating its objects in the runtime.
At a first glance this
approach could have much overhead, but you could see the advantages of this pattern ordering each TestUnit into TestCase
, getTestCases()
and a TestMethod1()
instead
of creating multiple test methods with their helper methods and naming them spontaneously.
Thanks for reading, for your comments, and for rating my article.
Points of
interest
My main point of interest is the optimization of the C# source code. If you have any suggestions
for this article please make a comment below. If you like this article please rate it with 5. If you're interested in writing clean (not stinky) code I recommend you visiting my latest
Open Source project for creating modular .NET applications - mcmframework.codeplex.com.
History
- Jan. 8 2013 - Advanced scenario added.
- Dec. 24 2012 - First release.