Click here to Skip to main content
12,063,645 members (68,255 online)
Click here to Skip to main content
Add your own
alternative version

Stats

19.4K views
264 downloads
26 bookmarked
Posted

A quick overview of NUnit tests

, 31 Jul 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
how to use NUnit to code tests

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.

Using the TestCase attribute

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

Using the TestCaseSource 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.

License

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

Share

About the Author

G.TR
Software Developer
France France
No Biography provided

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
_Noctis_31-Jul-14 13:56
professional_Noctis_31-Jul-14 13:56 
QuestionVery good article! Pin
Volynsky Alex4-Feb-14 10:35
professionalVolynsky Alex4-Feb-14 10:35 
Questiontest file cannot be downloaded Pin
fredatcodeproject29-Jan-14 4:58
memberfredatcodeproject29-Jan-14 4:58 
AnswerRe: test file cannot be downloaded Pin
pipo4429-Jan-14 5:39
memberpipo4429-Jan-14 5:39 
GeneralRe: test file cannot be downloaded Pin
fredatcodeproject29-Jan-14 5:48
memberfredatcodeproject29-Jan-14 5:48 
GeneralRe: test file cannot be downloaded Pin
pipo4429-Jan-14 6:03
memberpipo4429-Jan-14 6:03 
GeneralRe: test file cannot be downloaded Pin
fredatcodeproject29-Jan-14 23:13
memberfredatcodeproject29-Jan-14 23:13 
QuestionNice intro to auto-generating testcases Pin
John Brett29-Jan-14 2:08
memberJohn Brett29-Jan-14 2:08 
QuestionReally? Pin
cjb11028-Jan-14 22:17
membercjb11028-Jan-14 22:17 
AnswerRe: Really? Pin
pipo4429-Jan-14 0:49
memberpipo4429-Jan-14 0:49 
GeneralRe: Really? Pin
John Brett29-Jan-14 2:02
memberJohn Brett29-Jan-14 2:02 
GeneralRe: Really? Pin
FabricePA29-Jan-14 7:19
memberFabricePA29-Jan-14 7:19 
Questionimage can not display Pin
Southmountain28-Jan-14 13:45
memberSouthmountain28-Jan-14 13:45 
AnswerRe: image can not display Pin
pipo4429-Jan-14 0:39
memberpipo4429-Jan-14 0:39 

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.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.160204.4 | Last Updated 31 Jul 2014
Article Copyright 2014 by G.TR
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid