Test Driven Development Basic

So the task for the day is to build a calculator which could perform basic operations; they are:

Add / Subtract / Multiply and Divide

You want a quality delivery and the least possible chance of errors to occur, so in comes TDD. Instead of writing the code upfront from the beginning, you try and get the use cases for the calculator that you are going to build:

  1. It should add given numbers
  2. It should subtract given numbers
  3. It should multiply given numbers
  4. It should divide given numbers
    These were pretty standard ones that come to the mind upfront, best cases in short. Let us now try to come up with some of the negative scenarios and
  5. It should handle overflows and underflow correctly for all operations

Note: I am using NuGet to resolve the dependencies for my application.

Create a class library project; we want only one instance of the calculator to cater to the requests. So the first step would be to add a test to ensure that only one instance of the calculator is instantiated.

[TestFixture]
public class
SingletonProviderTests

{
    [Test]
    public void ShouldReturnSameInstance()
    {
        var instance1 = SingletonProvider<Calculator>.Instance;
        var instance2 = SingletonProvider<Calculator>.Instance; 
        Assert.IsInstanceOf<Calculator>(instance1);
        Assert.IsInstanceOf<Calculator>(instance2); 
        Assert.AreEqual(instance1,instance2);
    }
}


The above class as the name suggests is to test that instances that we are getting from our singleton provider are really the same instance.

Well, you should see all red right now in that class sinc we do not have the classes under test; this is where we will add them in our solution. Create a new Console project and add it to the solution.

Next we will add the SingletonProvider class first, as in:

public
sealed class SingletonProvider<T> where T : new()
{
    public static T Instance
    {
        get { return Nested.InnerInstance; }
    } 
    class Nested
    {
        static Nested()
        {               
        } 
        internal static readonly T InnerInstance = new T();
    }
}


The above class is a generic implementation of the singleton pattern; you can choose to implement your singleton as you desire.

Also, we need to have the Calculator class:

public class Calculator
{
    public Calculator(){ 
}

Well now if you build the solution and run the test, it should succeed:

testing1.gif

Well, that's good to start with; we have at least a tested code that ensures that only one instance of the Calculator class is there to play with.

Let us write some basic test cases for our calculator before we write some functionality for our calculator.

Add a new class in the unit test project:
CalculatorTests

Add a test method for asserting the behaviour of the add method:

[Test]
public void ShouldAddNumbers()
{
     var calculator = SingletonProvider<Calculator>.Instance;
     var result = calculator.Add(2, 3);
     Assert.AreEqual(5, result);           
}

Notice that Visual Studio indicates that the "Add" method has not been implemented, so put the cursor over the method and press Shift+Alt+F10;

testing2.gif

Press enter to generate the stub method in the Calculator class; this will create an Add method in the Calculator class but, with two arguments, what we want is that we should be adding all the numbers passed as arguments.
So, go to your calculator class and change the definition and signature of the method to:

public int Add(params int[] args)
{
    return args.Sum();
}

Now, since we have made our method take any number of arguments, we can make the test method also more exhaustive:

[Test]
public void ShouldAddNumbers()
{
     var calculator = SingletonProvider<Calculator>.Instance; 
     var result1 = calculator.Add(2, 3);
     var result2 = calculator.Add(2, 3, 4);
     var result3 = calculator.Add(2, 3, 4, 5); 

     Assert.AreEqual(5, result1);           
     Assert.AreEqual(9, result2);
     Assert.AreEqual(14, result3);
}

Now, let us run the test method and it should be green now:

testing3.gif

Great, this gives us confidence that whatever we have written so far, would work for the given set of inputs covered in our tests.

This was so far only the best-case tests that we wrote. We want some negative cases or edge cases also to be covered in our test class, such as:


[Test]
public void ShouldAddNegativeIntegers()
{
     var result1 = _calculator.Add(-2, -1);
     var result2 = _calculator.Add(-2, 4); 
     Assert.AreEqual(-3, result1);
     Assert.AreEqual(2, result2);
}

And

[Test]
public void ShouldHandleOneArgOnAdd()
{
    var result = _calculator.Add(1); 
    Assert.AreEqual(2, result);
}

So we have increased our confidence level by adding more test coverage for our add method and that ensures it will work for a large number of scenarios now.

As a special-use case that came to me, I was asked to make an Add method intelligent enough that if invoked with one argument, it should double the value and return the result, i.e.:

With only input 1, Add method should interpret it as 1+1 and return 2.

So, let me add a test case first for that to assert how my existing code behaves for such a case:

[Test]
public void ShouldHandleOneArgOnAdd()
{
    var result = _calculator.Add(1);
    Assert.AreEqual(2, result);
}

When I run this test, I get the following output:

testing4.gif

Clearly evident that, the existing code does not behave in the way as it was asked, so let us make code changes now to ensure this test passes successfully, and most importantly without introducing a new issue in the existing behaviour which works as expected.

public
int Add(params int[] args)

    var argCount = args.Count(); 
    if (argCount == 1)
       return Add(args[0], args[0]);
       return args.Sum();


Now we have made the code changes. Let us run our test suite to ensure everything works as expected and we intend to see a green go in our test result window:

testing5.gif

Great, so there you go, everything as expected achieved and also we didn't introduce any bug in the existing functionality.

So you see how useful is TDD as a practice, which ensures to many extent that:

  1. From test cases itself you could learn about the functionality (so less documentation is needed)
  2. Gives you as a developer more confidence on what you are writing
  3. You are only writing code that is required and hence keeps it short and simple.

Note: You can, as a practice, try out the same methodology for multiply, divide and subtract functionality in the calculator class, which is downloadable with this article.

In the next series, we will move from basic TDD to some more advanced concepts in TDD.


Similar Articles