13.1. Unit Testing

13.1.1. Introduction

The purpose of testing in software development is to establish developer’s confidence that the code continues to have high quality and less errors, hence to ensure that the requirements of the project are met.

Testing is a critical part of software development. In JetBrain’s The State of Developer Ecosystem 2023 survey [1], for example, 80% of the respondents say that “testing plays an integral role in their software development projects.” Also, 63% of respondents say they use Unit testing in their projects while 47% say they use Integration testing.

It is also noteworthy that while 41% of respondents say they have less than 1 tester/Quality Assurance Engineer per 10 developers, 32% of the respondents say that they have 1-3 testers/QA per 10 software developers. Testing, especially unit testing, therefore, is a common practice in real world software development.

Amazon AWS defines unit testing as:

Unit testing is the process where you test the smallest functional unit of code. Software testing helps ensure code quality, and it’s an integral part of software development. It’s a software development best practice to write software as small, functional units then write a unit test for each code unit. You can first write unit tests as code. Then, run that test code automatically every time you make changes in the software code. This way, if a test fails, you can quickly isolate the area of the code that has the bug or error. Unit testing enforces modular thinking paradigms and improves test coverage and quality. Automated unit testing helps ensure you or your developers have more time to concentrate on coding. [2]

The notion of unit testing is straightforward in principle. When you write a program in general, the program comprises what are properly known as units of development. Each language has its own definition of what units are but most modern programming languages view the class concept as the core unit of testing, although it can be done at lower level such as method and higher level such as package. Once we have a class, we can test it and all of the parts associated with it, especially its methods.

Unit testing is usually done by using testing frameworks. For C#, popular testing frameworks include: MSTest, NUnit, and xUnit.

13.1.2. Simple Testing

Assuming that you have a killer app like:

namespace IntroCSCS
{
   public class Calculator
   {
      public int Add(int x, int y)
      {
         return x + y;
      }
   }
}

Let’s say you want to know that this app is doing what you intend for it to do. The best way to achieve that validation is probably by providing a set of validation code that does nothing but running the app to test if the results come out right, such as the following:

 1namespace IntroCSCS
 2{
 3   internal class Program
 4   {
 5      static void Main(string[] args)
 6      {
 7         var calculator = new Calculator();
 8
 9         if (calculator.Add(15, 20) != 35)
10         {
11               throw new InvalidOperationException();
12         }
13         // else
14         // {
15         //     Console.WriteLine("OK");
16         // }
17      }
18   }
19}

Currently, there is only one method (Add) in the Calculator class. As you add more methods, you will need more cases and your Program.cs will become disorganized. This is where the established tooling comes in.

13.1.3. The Unit Testing Process

One of the fundamental principles of adopting unit testing is to follow a TDD (Test Driven Development) approach where you will:

  1. Write a failing test (to prove the point that the test works)

  2. Implement the functionality to pass the test

  3. Refactor the code while keeping the test green

../_images/tdd.png

As an example, you can prepare test cases such as the code below. Note that the forth method will fail.

 1public class BasicMaths
 2{
 3   public double Add(double num1, double num2) {
 4      return num1 + num2;
 5   }
 6   public double Subtract(double num1, double num2) {
 7      return num1 - num2;
 8   }
 9   public double divide(double num1, double num2) {
10      return num1 / num2;
11   }
12   public double Multiply(double num1, double num2) {
13      // To trace error while testing, writing + operator instead of * operator.
14      return num1 + num2;
15   }
16}

After the test cases are prepared, a test framework (e.g., xUnit, NUnit, or MSTest) is added to the project. From there, for NUnit, you can run the following command to add NUnit to your test project (xUnit and MSTest follow similar syntax) [3]:

dotnet add [location of your test csproj file] reference [location of the csproj file for project to be tested]

The method test code would look like the follows:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using BasicMath;
namespace BasicMathTest {

[TestClass]
public class UnitTest1 {
   [TestMethod]
   public void Test_AddMethod() {
            BasicMaths bm = new BasicMaths();
            double res = bm.Add(10, 10);
            Assert.AreEqual(res, 20);
      }
      [TestMethod]
   public void Test_SubstractMethod() {
            BasicMaths bm = new BasicMaths();
            double res = bm.Substract(10, 10);
            Assert.AreEqual(res, 0);
      }
      [TestMethod]
   public void Test_DivideMethod() {
            BasicMaths bm = new BasicMaths();
            double res = bm.divide(10, 5);
            Assert.AreEqual(res, 2);
      }
      [TestMethod]
   public void Test_MultiplyMethod() {
      BasicMaths bm = new BasicMaths();
      double res = bm.Multiply(10, 10);
      Assert.AreEqual(res, 100);
   }
}}

Note that, in the test code:

  • The method must be defined with the [TestMethod] attribute just above method name.

  • The method must having return type void.

  • The method cannot have any parameters.

Footnotes