👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Implementing TDD in C# .Net

Implementing TDD in C# .Net

Author - Abdul Rahman (Bhai)

TDD

2 Articles

Improve

In this article, let's learn about how to practice and implement TDD in C# .NET.

Note: If you have not done so already, I recommend you read the article on Introducing TDD in C# .NET.

Table of Contents

  1. Scenario
  2. Using TDD to write business logic
  3. Understanding the First Requirement
  4. Create a Red unit test
  5. Write Code to make test Green
  6. Refactor to improve the code
  7. Understanding the second Requirement
  8. Decouple the Dependencies
  9. Writing code to make the test pass
  10. Again refactor the code
  11. Summary

Scenario

Let's take a simple scenario where a user needs to book a ticket. The user needs to fill a form with basic details to book the ticket.

  • Ticket Booking
  • TicketBookingCore (TicketBookingRequestProcessor) (.Net Core Class Library)
  • TicketBookingCore.Tests (TicketBookingRequestProcessorTests) (XUnit test project)

The steps are,

  • Getting started with TDD.
  • Testing and implementing business logic.
  • Adding features in asp.net core app.

Let's first start with creating a test project named TicketBookingCore.Test for business logic. We will start writing the first failing test and continue from that. Then let's work on adding additional business logic by implementing tests and iterating through the TDD cycle. We have more complex requirements that will force us to decouple dependencies and we need to mock those classes while writing the test.

Finally let's see how to implement TDD with web project. It is your responsibility to check the user entered information and implement TDD.

Using TDD to write business logic

Requirements

  1. Response should contain the same values as request after booking.
  2. Booking should be saved to database.

Understanding the First Requirement

The user will submit a form to book a ticket which will make a call to TicketBookingRequestProcessor to book a ticket. The processor must return the same data after the booking is successful. To do this lets first think of the API. The TicketBookingRequestProcessor will use Book to book a ticket and it will receive TicketBookingRequest as input and return TicketBookingResponse as result.

first-requirement

This simple requirement is good to start with TDD.

Create a Red unit test

  1. Create a new C# XUnit test project named TicketBookingCore.Tests.
  2. As you need to test TicketBookingProcessor class, create a new class named TicketBookingRequestProcessorTests.
  3. Create a first test method as ShouldReturnTicketBookingResultWithRequestValues.
  4. Mark the method with [Fact] attribute to indicate it as a test.
  5. Now create a processor instance as TicketBookingRequestProcessor and press Ctrl + . to create a class in a new file.

namespace TicketBookingCore.Tests  
{  
    public class TicketBookingRequestProcessorTests  
    {  
        [Fact]  
        public void ShouldReturnTicketBookingResultWithRequestValues()  
        {  
            var processor = new TicketBookingRequestProcessor();  
        }  
    }  
}
        

Create a TicketBookingRequest with FirstName, LastName and Email properties and set the values and again press Ctrl + . to create the class in new file.


[Fact]  
public void ShouldReturnTicketBookingResultWithRequestValues()  
{  
    var processor = new TicketBookingRequestProcessor();  
  
    var request = new TicketBookingRequest  
    {  
        FirstName = "Abdul",  
        LastName = "Rahman",  
        Email = "abdulrahman@demo.com"  
    };  
} 
        

Add the following line TicketBookingResponse response = processor.Book(request); and press Ctrl + . to generate the Book method in TicketBookingRequestProcessor class.

Next, we need to assert if the input and output are equal.

To Assert the input and output, first, create TicketBookingResponse class with the same properties as TicketBookingRequest.

Modify the Book Method in TicketBookingRequestProcessor to return TicketBookingResponse.


internal class TicketBookingRequestProcessor  
{  
    public TicketBookingRequestProcessor()  
    {  
    }  
  
    internal TicketBookingResponse Book(TicketBookingRequest request)  
    {  
      throw new NotImplementedException();  
    }  
} 
        

We are all set with AAA (Arrange, Act, and Assert) of unit tests.


[Fact]  
public void ShouldReturnTicketBookingResultWithRequestValues()  
{  
    // Arrange  
    var processor = new TicketBookingRequestProcessor();  
  
    var request = new TicketBookingRequest  
    {  
        FirstName = "Abdul",  
        LastName = "Rahman",  
        Email = "abdulrahman@demo.com"  
    };  
  
    // Act  
    TicketBookingResponse response = processor.Book(request);  
  
    // Assert  
    Assert.NotNull(response);  
    Assert.Equal(request.FirstName, response.FirstName);  
    Assert.Equal(request.LastName, response.LastName);  
    Assert.Equal(request.Email, response.Email);  
} 
        

Press Ctrl + E, T to open Test Explorer. You can now see the test is listed in the test explorer. Now Click on the Run (Green Triangle) button. You can see that the test fails with NotImplementedException.

first-red-test

Great, we completed the 1st step of TDD (Red Phase).

Write Code to make test Green

Now let's go to 2nd step of TDD (Green Phase) and write the bare minimum code required to make the test pass.

Implemented the below code in Book Method of TicketBookingRequestProcessor class.


internal TicketBookingResponse Book(TicketBookingRequest request)  
{  
    return new TicketBookingResponse  
    {  
        FirstName = request.FirstName,  
        LastName = request.LastName,  
        Email = request.Email  
    };  
} 
        

That's it. Now again run the test. The test passes and turns green.

first-green-test

Great, we completed the 2nd step of TDD (Green Phase).

Refactor to improve the code

Now we are in the 3rd step of TDD (Refactor Phase). We can refactor and improve the code as follows,

  1. Create New .Net Core Class Library Named TicketBookingCore and move TicketBookingRequest, TicketBookingRequestProcessor and TicketBookingResponse into this project.
  2. Fix Namespaces of the files in TicketBookingCore project.
  3. Change the access modifier of all classes and methods to the public.
  4. Create a base class TicketBookingBase and move the properties from TicketBookingRequest and TicketBookingResponse and inherit from TicketBookingBase class.
  5. Add a reference to TicketBookingCore project in the TicketBookingCore.Tests project.
  6. Now Build the solution and run the tests again.
  7. The test should pass.

The new project structure should look like shown below:

solution-explorer

Great, we completed the 3rd step of TDD (Refactor Phase).

Now let's write another test using TDD to quickly verify that request is not null while calling a Book method.


[Fact]  
public void ShouldThrowExceptionIfRequestIsNull()  
{  
    // Arrange  
    var processor = new TicketBookingRequestProcessor();  
  
    // Act  
    var exception = Assert.Throws<ArgumentNullException>(() => processor.Book(null));  
  
    // Assert  
    Assert.Equal("request", exception.ParamName);  
} 
        

Now if you run the above from test explorer, the test will fail with NullReferenceException instead of ArgumentNullException, as shown below:

second-red-test

Now let's write the minimum required code to make the test pass.


public TicketBookingResponse Book(TicketBookingRequest request)  
{  
    if (request is null)  
    {  
        throw new ArgumentNullException(nameof(request));  
    }  
  
    return new TicketBookingResponse  
    {  
        FirstName = request.FirstName,  
        LastName = request.LastName,  
        Email = request.Email  
    };  
} 
        

Now run the test again and the test will pass.

second-green-test

The next step is to refactor. We don't have anything in Book method to refactor but that doesn't mean that we have nothing we can also refactor our tests.

Since we need TicketBookingRequestProcessor in both the test, we can remove that from both the test and move it to TicketBookingRequestProcessorTests constructor and use that in our test methods.


public class TicketBookingRequestProcessorTests  
{  
    private readonly TicketBookingRequestProcessor _processor;  
  
    public TicketBookingRequestProcessorTests()  
    {  
        _processor = new TicketBookingRequestProcessor();  
    }  
  
    [Fact]  
    public void ShouldReturnTicketBookingResultWithRequestValues()  
    {  
        // Arrange  
        var request = new TicketBookingRequest  
        {  
            FirstName = "Abdul",  
            LastName = "Rahman",  
            Email = "abdulrahman@demo.com"  
        };  
  
        // Act  
        TicketBookingResponse response = _processor.Book(request);  
  
        // Assert  
        Assert.NotNull(response);  
        Assert.Equal(request.FirstName, response.FirstName);  
        Assert.Equal(request.LastName, response.LastName);  
        Assert.Equal(request.Email, response.Email);  
    }  
  
    [Fact]  
    public void ShouldThrowExceptionIfRequestIsNull()  
    {  
        // Act  
        var exception = Assert.Throws<ArgumentNullException>(() => _processor.Book(null));  
  
        // Assert  
        Assert.Equal("request", exception.ParamName);  
    }  
} 
        

Understanding the second Requirement

Now we need to save the booking to the database. To save to database we need to modify the book method to save the booking to the database and return the booking request values.

second-requirement

Decouple the Dependencies

If you look at the above image, TicketBookingRequestProcessor has too many responsibilities. One is to process the booking request and another is to save it to the database.

But this violates the Single Responsibility Principle (SRP) - which says a class should have a single responsibility. So, to adhere to the SRP principle, we need to move the save to database logic to a separate class like TicketBookingRepository.

save-concrete

We can now save the TicketBooking object to the database using the TicketBookingRepository class. But now TicketBookingRequestProcessor depends on the TicketBookingRepository class to save to the database. This is not for the unit test, as the test needs to run in isolation. So here comes the Dependency Inversion Principle (DI), which says a class should always depend on abstraction, not on implementation. We can implement this by introducing a new interface ITicketBookingRepository. This interface implements TicketBookingRepository and saves to the database.

save-abstraction

So now TicketBookingRequestProcessor depends on ITicketBookingRepository and we don't need to worry about the database. This means to write a test we can create a mock (fake) object and use that to save to database and we can verify by this from the mock object that Save method is called at least once. This is how we can use interface to decouple the dependencies.

Now let's create a failing Red unit test,

  1. Create a new test method ShouldSaveToDatabase.
  2. Create a request object and pass it to the processor save method.

[Fact]  
public void ShouldSaveToDatabase()  
{  
    // Arrange  
    var request = new TicketBookingRequest  
    {  
        FirstName = "Abdul",  
        LastName = "Rahman",  
        Email = "abdulrahman@demo.com"  
    };  
  
    // Act  
    TicketBookingResponse response = _processor.Book(request);  
} 
        

Now to save to database, we need ITicketBookingRepository and that needs to be injected to TicketBookingRequestProcessor.

  1. Add ITicketBookingRepository to TicketBookingCore project
  2. Add Save() method with TicketBooking object as a parameter.
  3. Press Ctrl + . to create a TicketBooking class and inherit from the TicketBookingBase class.

public interface ITicketBookingRepository  
{  
    void Save(TicketBooking ticket);  
}
        

Now we need a mock object for ITicketBookingRepository. We can use the mock library to fake the repository.

  1. Add _ticketBookingRepositoryMock = new Mock<ITicketBookingRepository>(); to TicketBookingRequestProcessorTests constructor.
  2. Press Ctrl + . to download install Moq nuget package and repeat to add a private readonly field Mock<ITicketBookingRepository> _ticketBookingRepositoryMock for that repository.
  3. Pass the field to TicketBookingRequestProcessor constructor and press Ctrl + . to add that as a parameter to TicketBookingRequestProcessor class.

public class TicketBookingRequestProcessorTests  
{  
    private readonly Mock<ITicketBookingRepository> _ticketBookingRepositoryMock;  
    private readonly TicketBookingRequestProcessor _processor;  
  
    public TicketBookingRequestProcessorTests()  
    {  
        _ticketBookingRepositoryMock = new Mock<ITicketBookingRepository>();  
        _processor = new TicketBookingRequestProcessor(_ticketBookingRepositoryMock.Object);  
    }  
}  
  
public class TicketBookingRequestProcessor  
{  
    public TicketBookingRequestProcessor(ITicketBookingRepository ticketBookingRepository)  
    {  
    }  
} 
        

Now we need to write a setup in mock repository to make a callback when Save is called in that repository. And we need to assert that the Save method in the repository is called at least once and verifies the properties in the callback object.


[Fact]  
public void ShouldSaveToDatabase()  
{  
    // Arrange  
    TicketBooking savedTicketBooking = null;  
  
    _ticketBookingRepositoryMock.Setup(x => x.Save(It.IsAny<TicketBooking>()))  
        .Callback<TicketBooking>((ticketBooking) =>  
        {  
            savedTicketBooking = ticketBooking;  
        });  
  
    var request = new TicketBookingRequest  
    {  
        FirstName = "Abdul",  
        LastName = "Rahman",  
        Email = "abdulrahman@demo.com"  
    };  
  
    // Act  
    TicketBookingResponse response = _processor.Book(request);  
  
    // Assert  
    _ticketBookingRepositoryMock.Verify(x => x.Save(It.IsAny<TicketBooking>()), Times.Once);  
  
    Assert.NotNull(savedTicketBooking);  
    Assert.Equal(request.FirstName, savedTicketBooking.FirstName);  
    Assert.Equal(request.LastName, savedTicketBooking.LastName);  
    Assert.Equal(request.Email, savedTicketBooking.Email);  
} 
        

Now we are done with test setup. Run the test and the test should fail.

third-red-test

Writing code to make the test pass

Now we need to write the minimum code to make the test pass which is to create a private readonly field for ITicketBookingRepository make a call to Save method in repository inside the Book method in processor class.


public class TicketBookingRequestProcessor  
{  
    private readonly ITicketBookingRepository _ticketBookingRepository;  
  
    public TicketBookingRequestProcessor(ITicketBookingRepository ticketBookingRepository)  
    {  
        _ticketBookingRepository = ticketBookingRepository;  
    }  
  
    public TicketBookingResponse Book(TicketBookingRequest request)  
    {  
        if (request is null)  
        {  
            throw new ArgumentNullException(nameof(request));  
        }  
  
        _ticketBookingRepository.Save(new TicketBooking  
        {  
            FirstName = request.FirstName,  
            LastName = request.LastName,  
            Email = request.Email  
        });  
  
        return new TicketBookingResponse  
        {  
            FirstName = request.FirstName,  
            LastName = request.LastName,  
            Email = request.Email  
        };  
    }  
} 
        

Now if you run the test, the test will pass.

third-green-test

Again refactor the code

Now we can improve the code by doing some refactoring.

  1. According to Do Not Repeat Principle (DRY), we should avoid repeating the same code.
  2. In the TicketBookingRequestProcessorTests class, the same TicketBookingRequest object is constructed and used in two methods. This can be moved to the TicketBookingRequestProcessorTests constructor and can be made as a private readonly field.
  3. In the TicketBookingProcessor class, we can see that the mapping of properties is done twice. This mapping can be extracted into a generic method.

Now your code should look like as follows:


public class TicketBookingRequestProcessorTests  
{  
    private readonly TicketBookingRequest _request;  
    private readonly Mock<ITicketBookingRepository> _ticketBookingRepositoryMock;  
    private readonly TicketBookingRequestProcessor _processor;  
  
    public TicketBookingRequestProcessorTests()  
    {  
        _request = new TicketBookingRequest  
        {  
            FirstName = "Abdul",  
            LastName = "Rahman",  
            Email = "abdulrahman@demo.com"  
        };  
  
        _ticketBookingRepositoryMock = new Mock<ITicketBookingRepository>();  
  
        _processor = new TicketBookingRequestProcessor(_ticketBookingRepositoryMock.Object);  
    }  
  
    [Fact]  
    public void ShouldSaveToDatabase()  
    {  
        // Arrange  
        TicketBooking savedTicketBooking = null;  
  
        _ticketBookingRepositoryMock.Setup(x => x.Save(It.IsAny<TicketBooking>()))  
            .Callback<TicketBooking>((ticketBooking) =>  
            {  
                savedTicketBooking = ticketBooking;  
            });  
  
        // Act  
        _processor.Book(_request);  
  
        // Assert  
        _ticketBookingRepositoryMock.Verify(x => x.Save(It.IsAny<TicketBooking>()), Times.Once);  
  
        Assert.NotNull(savedTicketBooking);  
        Assert.Equal(_request.FirstName, savedTicketBooking.FirstName);  
        Assert.Equal(_request.LastName, savedTicketBooking.LastName);  
        Assert.Equal(_request.Email, savedTicketBooking.Email);  
    }  
}  
  
public class TicketBookingRequestProcessor  
{  
    private readonly ITicketBookingRepository _ticketBookingRepository;  
  
    public TicketBookingRequestProcessor(ITicketBookingRepository ticketBookingRepository)  
    {  
        _ticketBookingRepository = ticketBookingRepository;  
    }  
  
    public TicketBookingResponse Book(TicketBookingRequest request)  
    {  
        if (request is null)  
        {  
            throw new ArgumentNullException(nameof(request));  
        }  
  
        _ticketBookingRepository.Save(Create<TicketBooking>(request));  
  
        return Create<TicketBookingResponse>(request);  
    }  
  
    private static T Create<T>(TicketBookingRequest request) where T : TicketBookingBase, new ()  
    {  
        return new T  
        {  
            FirstName = request.FirstName,  
            LastName = request.LastName,  
            Email = request.Email  
        };  
    }  
} 
        

Now run all the tests. All the tests should pass.

all-green-test

Summary

In this article, we learned how to implement TDD in C# .Net applications. We learned the TDD principle, advantages and disadvantages of TDD, understanding the requirements and starting from the test project then slowly building the actual requirement. We also learned how to decouple dependencies and mock them in a unit test. TDD is all about iterating the RED, GREEN and Refactor cycle over and again to develop our requirement. This is a demo project and it has a lot of scope for improvement. We learned TDD with XUnit Project, but the same can be applied to NUnit or MSTest projects as well.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • TDD
  • Test Driven Development