Click here to Skip to main content
15,867,330 members
Articles / Programming Languages / C#

Pragmatic Unit Tests using "yield return" for providing test cases

Rate me:
Please Sign up or sign in to vote.
4.33/5 (5 votes)
5 Feb 2013CPOL3 min read 38.4K   120   14   11
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)


Written By
Software Developer (Senior) Polenter - Software Solutions
Germany Germany
I'm C# developer from Cologne, Germany. Here I owe a small software company. My hobby is general optimization - natural talent of all lazy people Wink | ;-)

Comments and Discussions

 
Questionunit test Pin
Member 124464206-Jul-18 1:34
professionalMember 124464206-Jul-18 1:34 
QuestionAgainst the methodology Pin
Ranjan.D8-Jan-13 6:03
professionalRanjan.D8-Jan-13 6:03 
AnswerRe: Against the methodology Pin
Pawel idzikowski8-Jan-13 9:18
Pawel idzikowski8-Jan-13 9:18 
GeneralRe: Against the methodology Pin
Ranjan.D8-Jan-13 10:19
professionalRanjan.D8-Jan-13 10:19 
QuestionAn alternative - NUnit Pin
Rui Jarimba7-Jan-13 5:41
professionalRui Jarimba7-Jan-13 5:41 
AnswerRe: An alternative - NUnit Pin
Pawel idzikowski7-Jan-13 20:03
Pawel idzikowski7-Jan-13 20:03 
QuestionJust use nunit Pin
Sacha Barber30-Dec-12 21:46
Sacha Barber30-Dec-12 21:46 
GeneralMy vote of 4 Pin
Don Kackman30-Dec-12 14:26
Don Kackman30-Dec-12 14:26 
QuestionNot sure this approach increases manageability. Pin
Don Kackman30-Dec-12 14:24
Don Kackman30-Dec-12 14:24 
AnswerRe: Not sure this approach increases manageability. Pin
Pawel idzikowski30-Dec-12 21:09
Pawel idzikowski30-Dec-12 21:09 
GeneralMy vote of 4 Pin
fredatcodeproject30-Dec-12 1:45
professionalfredatcodeproject30-Dec-12 1:45 
I cannot download your demo file.
Can you fix it ?

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.