Think with Enlab

Diving deep into the ocean of technology

Stay Connected. No spam!

How to apply Test-Driven Development with practical examples

 

Over the last few years, test-driven development (a.k.a. TDD) has grown in popularity. Many programmers have tried and failed with this technique, concluding that TDD is not worth the effort. Yet, in this article, we will go through what test-driven development is, explore why people use it, and reach out how to apply TDD effectively with practical examples.


Basic concept

What is Test-Driven Development?

Test-driven development is an approach in software development in which you write tests first before implementing a feature. Kent Beck created it in the late 1990s as part of Extreme Programming. It reverses the traditional technique that leads the software development by designing the code architecture first, writing code, then iteratively doing testing until it passes all the test cases that are defined after the development has been done. In TDD, we will focus on ‘what’ to implement first, instead of ‘how’ to achieve it. TDD focuses on code design and pursues a better quality of code as well.

 

Advantages of using Test-Driven Development

  • TDD guarantees that all the codes are well-tested. A key characteristic of a test is that it can fail, and the development team verifies that each new test fails. This will bring high confidence when we release the software to production.
  • Less code: You only need to write the minimum lines of code that pass the test. It will help to reduce code duplication, enabling faster innovation and continuous delivery.
  • Easier to maintain: TDD makes your code flexible and extensible. The code can be refactored or moved with minimal risk of breaking code. Since refactoring is an important step repeated in every iteration of the TDD cycle, the code will be continuously maintained.

 

Disadvantages of using Test-Driven Development

TDD also has some disadvantages that you need to consider depending on your project size.

  • It requires more time to write the test. Since the test should be pre-defined, it would take time for our developers to write test cases and so the development time can be longer than usual.
  • It requires developers more experienced. It means that developers working with TDD need to have an understanding of this technique, have enough skill to write failing tests, as well as the ability to follow the process strictly.

 

How to apply Test-Driven Development?

There are 3 steps to implementing TDD:

  • Step 1: Understand the requirement and write code that makes the test fail.
  • Step 2: Write code to make the test pass.
  • Step 3: Refactor the code you have just written to make it more readable and maintainable.

These 3 steps are often described as a Red-Green-Refactor (RGR) cycle as in the figure below.

Test driven development cycle

 

The purpose of the RGR cycle is to guarantee that you only write sufficient code to make the test pass, and meanwhile keep the code well structured. To ensure that, Robert C. Martin defines the three laws of TDD:

“First law: You may not write production code until you have written a failing unit test.
Second law: You may not write more of a unit test that is sufficient to fail, and not compiling is failing.
Third law: You may not write more production code that is sufficient to pass the currently failing test.”

 

TDD implementation in a .NET Core application with code examples

Let us take an example of how TDD is applied in a project. From Visual Studio, create a .NET Core Web API named TddExample. By default, it will create an empty solution with a default controller named “WeatherForecastController”. But if it did not create due to an older version of Visual Studio, let us create it manually and replace it with the following code.

WeatherForecastController:

 namespace TddExample.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly List<WeatherForecast> Data = new List<WeatherForecast>
        {
            new WeatherForecast(DateTime.Now, 16, "Freezing"),
            new WeatherForecast(DateTime.Now.AddDays(1), 20, "Cold"),
            new WeatherForecast(DateTime.Now.AddDays(3), 21, "Cold"),
            new WeatherForecast(DateTime.Now.AddDays(3), 24, "Mild"),
            new WeatherForecast(DateTime.Now.AddDays(4), 38, "Sweltering"),
            new WeatherForecast(DateTime.Now.AddDays(5), 39, "Scorching"),
            new WeatherForecast(DateTime.Now.AddDays(6), 40, "Scorching"),
            new WeatherForecast(DateTime.Now.AddDays(7), 26, "Mild"),
            new WeatherForecast(DateTime.Now.AddDays(8), 29, "Warm"),
            new WeatherForecast(DateTime.Now.AddDays(9), 30, "Hot"),
            new WeatherForecast(DateTime.Now.AddDays(10), 31, "Hot"),           
            new WeatherForecast(DateTime.Now.AddDays(11), 32, "Balmy"),
            new WeatherForecast(DateTime.Now.AddDays(12), 27, "Warm"),
            new WeatherForecast(DateTime.Now.AddDays(13), 22, "Mild"),
        };
      
        public WeatherForecastController()
        {
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            return null;
        }
    }
}

and WeatherForecast class:

namespace TddExample
{
    public class WeatherForecast
    {
        public DateTime Date { get; set; }

        public int TemperatureC { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

        public string Summary { get; set; }

        public WeatherForecast(DateTime date, int tempC, string summary)
        {
            Date = date;
            TemperatureC = tempC;
            Summary = summary;
        }
    }
}

Assuming we have the following requirement for the HttpGet API to get a weather forecast:

  • The API should return the weather forecast information for the next seven days, including the current day.
  • If a day with a temperature in Celsius greater than 39, the Summary should be added with the warning text: “Do not go outside at noon!”.

First, let’s add an NUnit Test project to the solution, named TddExample.Test. Rename UnitTest1.cs with the name WeatherForecastControllerTest.cs, and replace it with the following code.

namespace TddExample.Test
{
    public class WeatherForecastControllerTest
    {
        [SetUp]
        public void Setup()
        {
        }
    }
}

Now everything is ready for the implementation of TDD. The first step of TDD is to write a failed test. Add the following method to the Test class we have just created above.

[Test]
        public void Get()
        {
           // Arrange
            var controller = new Controllers.WeatherForecastController();

            // Act
            var nextSevenDaysForecast = controller.Get();

            // Assert
            // 1. The API returns enough 7 days data
            Assert.AreEqual(7, nextSevenDaysForecast.Count());
            // 2. Data is 7 days in-a-row from today
            int index = 0;
            for (DateTime date = DateTime.Now.Date; date <= DateTime.Now.AddDays(6); date = date.AddDays(1))
            {
                Assert.AreEqual(nextSevenDaysForecast.ElementAt(index).Date.Date, date.Date);
                index++;
            }
        }

Note: You can read more about the Arrange-Act-Assert pattern.


At this moment, if we try to run the test, we will get the failed result with this error: System.ArgumentNullException. It is the correct result because the method Get() returns null, so the .Count() method raised an exception. We have done step 1: writing the failed test (the ‘Red’ step).

1. TDD example in .NET

Now, as we have the failed test, the next step is to make it ‘Green’ by implementing the method to make it pass the test. Edit the Get method in the Controller to make it returns the next seven days' data.

[HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            return Data.Where(data => data.Date < DateTime.Now.AddDays(6));
        }

As we have implemented the code in order to make the test pass, let’s re-run the test to check the result. You might beware that it is still failed.

2. Example of test driven development

By looking at the Error Message, Expected: 2022-05-27 00:00:00 But was: 2022-05-26 00:00:00, we know one of our Assert conditions has not passed. Re-check the code implementation to find what causes the test to be failed and correct it. After a few seconds, you will see that it is because the data is incorrect: the 3rd day in our mock Data should be the next two days, not three days.

3. Example of TDD

Edit it to AddDays(2) and re-run it to see if the test is passed now.

4. Test driven development example

Now you realize that TDD will help us ensure the code's accuracy. It will ensure the program runs correctly as long as all the tests are well-defined, possible falling cases are covered, and all tests are passed after implementation.

Continue with the second requirement, let’s repeat the RGR cycle by defining a second test method to check if the API returns the correct data that matches the requirement:

[Test]
        public void GetWithWarning()
        {
            // Arrange
            var controller = new Controllers.WeatherForecastController();

            // Act
            var nextSevenDaysForecast = controller.Get();

            // Assert
            if(nextSevenDaysForecast.Any(_ => _.TemperatureC > 39))
            {
                const string Warning = "Do not go outside at noon!";
                Assert.IsTrue(nextSevenDaysForecast
                    .Where(_ => _.TemperatureC > 39)
                    .All(data => data.Summary
                    .EndsWith(Warning)));
            }
        }

And like what we did before, first re-run to see if it’s failed.

5. Code sample of test driven development

Then, implement the code in the Controller to get it passed.

[HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var nextSevenDaysForecast = Data.Where(data => data.Date < DateTime.Now.AddDays(6));

            const string Warning = "Do not go outside at noon!";
            nextSevenDaysForecast.Where(_ => _.TemperatureC > 39).ToList().ForEach(date =>
            {
                date.Summary = string.Concat(date.Summary, Warning);
            });

            return nextSevenDaysForecast;
        }

Re-run to see if the 2 tests are passed.

6. Result of code sample TDD

Although the code in this example is simple enough that the Refactor phase is not required, in production, you should be aware of refactoring continuously after a test has passed to keep the code well-structured and maintainable.

 

Final thoughts
Test-driven development is a process of developing, running automated tests, and refactoring before the actual development of the application. It enables developers to build solid and robust software with clearer and more understandable code. Hopefully, you have an overview of TDD and know how to apply it to your software development practice.

Happy coding!

 

CTA Enlab Software

About the author

Tuan Do

Hi, I'm Tuan Do. As a software engineer at Enlab, I love coding and developing software that brings value to our clients. I specialize in back-end technologies such as .NET, SQL Server, and MongoDB. I’m also a fan of cloud computing platforms like AWS. When not at work, I enjoy reading books and doing technical research.
Frequently Asked Questions (FAQs)
What is Test-Driven Development (TDD) and how does it work?

Test-Driven Development is a software development approach where tests are written before the actual code. It involves writing a test that fails, writing the minimal code to pass the test, and then refactoring the code for optimization. This process, known as the Red-Green-Refactor cycle, emphasizes the design and requirements before writing the code.

What are the advantages of using Test-Driven Development?

The advantages include ensuring that all code is well-tested, which boosts confidence at the time of release. TDD leads to less code being written, reducing code duplication and enabling faster innovation. It also results in code that is easier to maintain and refactor.

Are there any disadvantages to Test-Driven Development?

The main disadvantages include the additional time required to write tests upfront and the need for experienced developers who are proficient in TDD. This can extend the development time and may require a learning curve for the development team.

Can you describe the Red-Green-Refactor (RGR) cycle in TDD?

The RGR cycle involves three steps: writing a test that initially fails (Red), writing code to make the test pass (Green), and then refactoring the code for efficiency and readability (Refactor). This cycle ensures that only necessary code is written and maintains good code structure.

How is TDD applied in a .NET Core application with practical examples?

In the provided example, a .NET Core Web API project is used to demonstrate TDD. The process involves creating tests for specific functionalities, such as retrieving weather forecast data and adding warnings for extreme temperatures. The tests are run to fail initially, then code is written to pass the tests, followed by any necessary refactoring.

Up Next

Big Data Technologies Transforming Software Development
July 05, 2024 by Dat Le
In the rapidly evolving world of software development, Big Data stands out as a transformative force....
June 27, 2024 by Dat Le
In today's rapidly evolving digital landscape, secure coding practices are paramount to safeguarding applications from a...
June 20, 2024 by Dat Le
In the rapidly evolving digital landscape, the role of User Interface (UI) and User Experience (UX)...
Leveraging UX Design Principles in Software Development
June 17, 2024 by Dat Le
In the dynamic world of software development, one element has emerged as crucial to success: User...
Roll to Top

Can we send you our next blog posts? Only the best stuffs.

Subscribe