Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Pragmatic Unit Tests using yield return for providing test cases

4.33/5 (5 votes)
5 Feb 2013CPOL3 min read 39.8K   120  
How to gain better control over the testing process reducing the amount of test methods in a unit test.

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.  

C#
public class Calculator
{
    /// <summary>
    /// Divides dividend by the divisor
    /// </summary>
    /// <param name="dividend">is divided by divisor</param>
    /// <param name="divisor">divides the dividend</param>
    /// <returns></returns>
    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.

C#
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        //
        // TODO: Add test logic here
        //
    }
}

In the next step we define a test case as a private class of UnitTest1.

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

C#
private IEnumerable<TestCase> getTestCases()
{
    // both Dividend and Diviser are 0
    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().

C#
[TestMethod]
public void TestMethod1()
{
    foreach (var testCase in getTestCases())
    {
        // Create the tested class
        var c = new Calculator();
        try
        {
            // invoke the tested method
            var result = c.Divide(testCase.Dividend, testCase.Divisor);

            // check the result
            Assert.AreEqual(testCase.ExpectedResult, result);
            Assert.IsNull(testCase.ExpectedExceptionType);
        }
        catch (Exception ex)
        {
            // an error has occured
            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.

C#
private IEnumerable<TestCase> getTestCases()
{
    (...)

    // Dividend is 0, Diviser is > 0
    tc = new TestCase(0, 1, 0, null);
    tc.Description = "Dividend is 0, Diviser is > 0";
    yield return tc;

    // Dividend is > 0, Diviser is 0
    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.

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

C#
public interface ITimeProvider
{
    DateTime Now { get; }
}

In production we use an instance of SystemTimeProvider.

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

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

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

C#
private IEnumerable<TestCase> getTestCases()
{
    // today is sunday
    var timeProvider = new ConstantTimeProvider(new DateTime(2013, 01, 06));
    var tc = new TestCase(timeProvider, false, "today is sunday");
    yield return tc;

    // today is monday
    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().

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

License

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