Creating a calculator application in TDD

C# nUnit TDD

There are many ways and principles how a software project can be developed. These ideas usually came from different eras, as the software market evolved during the years. In this article I write about TDD, what is that and how to apply this concept to a real coding project to meet the project requirements.


What is TDD?

TDD stands for Test Driven Development, but what does that mean? Basically, the general (or old fashioned?) way of a software development in big steps goes like this: 1) writing the software 2) testing the software 3) release the final software product.

Software development models

The most fundamental development model is known as Waterfall-model, which was published around the ’70s. It’s not a strict date, since many pre-publications was made before even in 1956. The waterfall model is a breakdown of project activities into linear sequential phases, where each phase depends on the deliverables of the previous one and corresponds to a specialisation of tasks – according to Wikipedia. As the name suggests these sequential parts are coming one by one, and after all (at the end of the waterfall) these is no loopback, the sequence ends – which means the final product is released. In the model there is a “maintenance” part, which means that any problems occur after the release these can be fixed etc., but the core of the development is finished.

As we can see, in this procedure – as I wrote before – we code the software and then test it afterwards. However, what happens if we switch the testing and the coding part? This is how TDD was born.

Requirements into test cases

TDD means, that first we have the test cases and then according to these cases we will write the code. This was a completely new concept, but has some advantages in many cases. One key point is connected to requirements. Let’s say we have to develop a calcolator application. The customer let us know what features does he need, eg. two numbers should be added together, two numbers should be divided and so on. In TDD these requirements immediately can be formed to test cases, so right after we have the requirement list from the customer we can create these formally in code.

After the cases were created we have to write the application’s code as well. After we finished a subpart the test cases run, and the output shows us if we correctly developed the function or not. If not, we modify the function to pass the test. If it passed, we do a small refactoring on the code, but basically we are good to go. This is a cyclic method, when we repeat something until it’s not finished. The image below shows the TDD process (source).

tdd-cycle

Sidenote 1.: in normal / bigger environments completely separated people write the application code and the testing code. So, keep this in mind, even though I wrote always ‘we’ here in the article. In any case, TDD can be used only for ourselves so don’t be afraid trying it out.

Sidenote 2.: even though the the code passed the test it doesn’t mean that those lines of code are pretty, good to read, easy to understand and easy to extend. TDD usually focuses on short, rapid development cycles. Tests can be added, code can be re-adjusted to pass / fit new tests. But it does not care about code-reusability by standards (only if we care about).

Development

In this section we will create a small calculator application based on TDD in C#. At first we will define the function list (what we need as the customer). Then we will create the tests for that. Then we will write the application itself.

Requirements

What we will need in the calculator application:

  • the calculator should be able to add together two integers (A and B)
    • IF integer A and B is smaller than 1000
    • IF integer A and B is bigger than 1000, addition should not be possible
    • negative numbers should be interpreted according to math rules
  • the calculator should be able to divide integer A by integer B
  • the calculator should be able to handle division by zero
  • the calculator should be able to have a history about which commands and parameters were called previously
  • the calculator should be able to delete previously saved history

Creating the environment

For this small project we will create a C# console application (.Net Framework, not .Net Core). Add a new class library (also .Net Framework) project to the solution. We will use nUnit and nUnit TestAdapter, so add these under NuGet repository to the class lib project.

nunit

Iteration #0 – preparation

Before creating the test class we have to create the Calculator class itself, alongside with a few child Exception classes. This is needed because in the test methods we will use these objects. Methods (without method body) and properties can be added also, to get rid of the errors in the test methods (no such method exists etc. errors). Here I simply skip this part, later we will write the full methods. In a scenario when full tests are written by someone else and only given to us, this problem does not occur. So handle this part with this in mind.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TooBigInputException: Exception
{
  public TooBigInputException(string msg) : base(msg) { }
}

public class ZeroDivisionException : Exception
{
  public ZeroDivisionException(string msg) : base(msg) { }
}

public class Calculator
{
  // Add stuff here later on...
}

Iteration #1 – coding test cases

Firstly we have to form the test cases acccording to the requirements. It can be done by ourselves (if we work on our own) or can be done by a fully random person. One thing is for sure: we have to fit our application to these cases. To match the requirements we will create separate methods for each functionality test.

Sidenote: I’m assuming that you the reader is familiar with basic testing concepts, so I don’t talk about that deeply here but a few links I gathered together if you are interested:

Test case explanation

Below this section the full code can be found for the tests. Let’s see what each method will test:

  • TestAddition_SimpleParams: test if simple addition works correctly
  • TestAddition_NegativeParams: test if addition with negative parameters works according to math rules
  • TestAddition_TooBigParams: test if addition with too big parameters is not possible
  • TestDivision_ThrowsException: test if dividing by zero is not possible
  • TestDivision_CorrectReturnValue: test if simple division works correctly
  • TestClearMemory: test if calculator history deletion works correctly
  • TestHistory: test if calculator history works correctly

CalculatorTest.cs code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
[TestFixture]
public class CalculatorTest
{
  private Calculator calc;

  [SetUp]
  public void Init()
  {
    calc = new Calculator();
  }

  [TestCase(10, 20, 30)]
  [TestCase(9, 21, 30)]
  [TestCase(22, 8, 30)]
  public void TestAddition_SimpleParams(int a, int b, int result)
  {
    Assert.That(calc.Addition(a, b), Is.EqualTo(result));
  }

  [TestCase(30, -8, 22)]
  [TestCase(-8, 30, 22)]
  [TestCase(-20, -8, -28)]
  public void TestAddition_NegativeParams(int a, int b, int result)
  {
    Assert.That(calc.Addition(a, b), Is.EqualTo(result));
  }
 
  [Test]
  public void TestAddition_TooBigParams()
  {
    Assert.Throws<TooBigInputException>(() => calc.Addition(10, 1001));
    Assert.Throws<TooBigInputException>(() => calc.Addition(1001, 1001));
    Assert.Throws<TooBigInputException>(() => calc.Addition(1001, 10));
  }

  [Test]
  public void TestDivision_ThrowsException()
  {
    Assert.Throws<ZeroDivisionException>(() => calc.Division(10, 0));
    Assert.Throws<ZeroDivisionException>(() => calc.Division(100, 0));
    Assert.Throws<ZeroDivisionException>(() => calc.Division(-300, 0));
  }

  [TestCase(10, 5, 2)]
  [TestCase(10, 2, 5)]
  [TestCase(10, 4, 2.5)]
  public void TestDivision_CorrectReturnValue(int a, int b, double result)
  {
    Assert.That(calc.Division(a, b), Is.EqualTo(result));
  }

  [Test]
  public void TestHistory()
  {
    calc.Addition(100, 200);

    Assert.That(calc.History[calc.History.Count-1].Equals("addition (100,200)"));

    calc.Division(10, 5);

    Assert.That(calc.History[calc.History.Count-1].Equals("division (10,5)"));
  }

  [Test]
  public void TestClearMemory()
  {
    calc.Addition(99, 2);
    calc.Addition(-10, -30);

    calc.ClearMemory();

    calc.Addition(100, 200);
    calc.Addition(1, 3);
    calc.Division(10, 5);

    Assert.That(calc.History[0].Equals("addition (100,200)"));
    Assert.That(calc.History[1].Equals("addition (1,3)"));
    Assert.That(calc.History[2].Equals("division (10,5)"));
  }
}

Iteration #2 – coding the application

After we created the tests or somebody already has written it to us and we included them to our project, we can work in the real application. We already have the class created, so add the followings:

  • List<string>: this will hold us the used commands as history
  • Addition: adding two integers together
  • Division: divide parameter_A by parameter_B
  • ClearMemory: purge items from the history list

Calculator.cs code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Calculator
{
  public List<string> History { get; private set; }

  public Calculator()
  {
    this.History = new List<string>();
  }

  public int Addition(int a, int b)
  {
    return a + b;
  }

  public double Division(int a, int b)
  {
    return (double)a / b;
  }

  public void ClearMemory()
  {
    this.History.RemoveRange(1, this.History.Count);
  }
}

After we write the code, run the tests in the Test Explorer. A few seconds later we will see the following output:

Test output

test-1

Most methods worked correctly (mainly simple divisioin and addition) but special cases (exception handling etc.) failed. So, since we are doing TDD our next step is to adjust our application codebase to pass the tests.

Iteration #3 – fixing the code

Start with examining the test outputs, what are the problems, what is the input and what is the output. Compare it to the expected output.

Based on that we will find out that:

  • in division, divide by zero is not handled
  • history is not used (neither in addition nor in division) or not working correctly (we will see later)
  • in addition too big parameters not handled

So, fix these lines of code accordingly, updated code is here below. After it’s done, re-run all the tests and check output.

Calculator.cs code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Calculator
{
  public List<string> History { get; private set; }

  public Calculator()
  {
    this.History = new List<string>();
  }

  public int Addition(int a, int b)
  {
    return a + b;
  }

  public double Division(int a, int b)
  {
    return (double)a / b;
  }

  public void ClearMemory()
  {
    this.History.RemoveRange(1, this.History.Count);
  }
}

Test output

test-2

Iteration #4 – fixing the code

Basically we repeat the 3rd iteration, fixing and adjusting the codebase to pass the tests. We accidentally indexed from 1 instead of 0 in ClearMemory method. Adjust that to this and re-run the tests.

1
2
3
4
public void ClearMemory()
{
  this.History.RemoveRange(0, this.History.Count);
}

Test output

test-3

Iteration #5 – ending

We modified the code correctly, so all test cases are covered (this is what we call test-coverage) and the application corresponds to the requirements, it could be released.

Conclusion

We learned what is Test Driven Development and how to use it to improve code iteration by iteration. We coded a small application based on this principle.

I’d suggest you to experiment with more complex exercises to get a deeper experiment in this. I believe this small example is a good introduction to get the feeling of TDD.

TDD for bigger projects

A good example is to create a layered program where you have a repository layer and a logic layer at least. Using interfaces we can firstly create logic interface, with all the needed methods and properties defined. We can create the tests using the interface reference. And then running the tests, improving the code; running tests again, etc. etc.. For bigger projects interface usage is a must, especially if we want to write tests.

Downloadables

The full codebase can be downloaded from my repository.

Or clone it: https://github.com/siposm/tdd-development.git