Introduction
Hello all, welcome to Test Driven Development. I’m Abdul Rahman, a Senior Software developer, and a freelance solution architect. I used to develop products/enterprise applications using C# and .Net.
In this article, we will learn how to use TDD in C# to develop requirements in .NET applications. TDD is a very powerful approach to building robust software. Before we develop the feature, we will write a unit test for the feature which means the unit test will drive our feature development.
Let’s cover
- Basics of Test-Driven Development
- Using TDD to write business logic
- Decoupling dependencies
By the end of this article, you will learn how to implement TDD in your .NET web application.
Prerequisites
You need to know the basics of C# and unit testing. You can find the link for the source code and follow along with me.
Basics of Test-Driven Development
Let’s see what is test-driven Development and explain to you the project scenario.
Test-Driven Development, or TDD for short, is a method used to write tests before we start our implementation. Before you start, you might list the requirements that need to be fulfilled in your application.
Then you take the first requirement and write a failing test. The test fails and it is RED as you haven’t developed it yet. But the test describes already what your code should do to fulfill the requirement. Now you need to make the test GREEN by writing the necessary code to make the test pass. After you write the code to make the test green you need to Refactor the code. Maybe you can simplify the code or extract a few code lines into a method to make the code more readable and maintainable. After you are done with the first requirement, you can continue with the next requirement. This means you iterate the entire cycle (Red -> Green -> Refactor) for another requirement and so on. So the tests are driving your development. This is the heart of TDD and is known as the TDD cycle.
TDD means writing tests to implement a requirement and continuously iterate through RED GREEN and REFACTOR cycles.
Advantages of Test-Driven Development
- TDD makes you think about the needed API from the beginning. You need to think about what classes, properties, and APIs are needed. This will usually lead to great API design.
- After you know the class and properties, another big advantage is that you need to think about what the code should do rather than how it should be done. As you start with the test you don’t need to have any idea about implementation. You just need to write a test for what the code should do. After writing the test you can think of requirements and their development.
- While thinking of your requirements, you get quick feedback about your requirements by running the test. The fact that you get quick feedback means you even don’t need a fully working application at all. You just need a class library to build your business logic and don’t need the entire project.
- This helps you create modular code. You can decouple the dependencies from the beginning and TDD makes you do that from the beginning. This decoupling of dependency makes you write modular code by isolating the dependencies like a database that is not ready yet and the web API isn't ready when beginning the development.
- This leads to a maintainable codebase, as you will have one test per requirement. You can write code to add new functionality and run all the unit tests to ensure that the existing code doesn’t break. You can be confident about your new code as well as the existing code.
- These tests will serve as good documentation. For example, the test for the code written by others will help you understand why the code has been written.
Disadvantage of Test-Driven Development
The only disadvantage is that TDD is not so easy to start by writing tests for beginners. In fact, TDD is an art that every developer should master.
Scenario
Let’s take a simple scenario where a user needs to book a ticket. The user needs to fill out a form with basic details to book the ticket.
Ticket Booking Solution
TicketBookingCore (TicketBookingRequestProcessor) (.Net Core Class Library)
TicketBookingCore.Tests (TicketBookingRequestProcessorTests) (XUnit test project)
- 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 a 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 the request after booking.
- 2. Booking should be saved to the database.
Understand 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 let's 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 a result.
This simple requirement is good to start with TDD.
Create a Red unit test
- Create a new C# XUnit test project named TicketBookingCore.Tests.
- As you need to test TicketBookingProcessor Class, create a new class named TicketBookingRequestProcessorTests.
- Create a first test method as ShouldReturnTicketBookingResultWithRequestValues.
- Mark the method with the [Fact] attribute to indicate it as a test.
- Now create a processor instance as TicketBookingRequestProcessor and press Ctrl + . to create a class in a new file.
Code
namespace TicketBookingCore.Tests
{
public class TicketBookingRequestProcessorTests
{
[Fact]
public void ShouldReturnTicketBookingResultWithRequestValues()
{
var processor = new TicketBookingRequestProcessor();
}
}
}
Create a TicketBookingRequest with FirstName, LastName, and Email properties, set the values, and again press Ctrl + . to create the class in the new file.
Code
[Fact]
public void ShouldReturnTicketBookingResultWithRequestValues()
{
var processor = new TicketBookingRequestProcessor();
var request = new TicketBookingRequest
{
FirstName = "Abdul",
LastName = "Rahman",
Email = "[email protected]"
};
}
Add the following line TicketBookingResponse response = processor.Book(request); and press Ctrl + . to generate the Book method in the TicketBookingRequestProcessor class.
Next, we need to assert if the input and output are equal.
To Assert the input and output, first, create a TicketBookingResponse class with the same properties as TicketBookingRequest.
Modify the Book Method in TicketBookingRequestProcessor to return TicketBookingResponse.
Code
internal class TicketBookingRequestProcessor
{
public TicketBookingRequestProcessor()
{
}
internal TicketBookingResponse Book(TicketBookingRequest request)
{
throw new NotImplementedException();
}
}
We are all set with AAA (Arrange, Act, and Assert) unit tests.
Code
[Fact]
public void ShouldReturnTicketBookingResultWithRequestValues()
{
// Arrange
var processor = new TicketBookingRequestProcessor();
var request = new TicketBookingRequest
{
FirstName = "Abdul",
LastName = "Rahman",
Email = "[email protected]"
};
// 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.
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 the Book Method of TicketBookingRequestProcessor class.
Code
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.
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,
- Create a New .Net Core Class Library Named TicketBookingCore and move TicketBookingRequest, TicketBookingRequestProcessor, and TicketBookingResponse into this project.
- Fix the Namespaces of the files in the TicketBookingCore project.
- Change the access modifier of all classes and methods to the public.
- Create a base class TicketBookingBase and move the properties from TicketBookingRequest and TicketBookingResponse and inherit from TicketBookingBase class.
- Add a reference to the TicketBookingCore project in the TicketBookingCore.Tests project.
- Now Build the solution and run the tests again.
- The test should pass.
The new project structure should look like shown below:
Great, we completed the 3rd step of TDD (Refactor Phase).
Now let’s write another test using TDD to quickly verify that the request is not null while calling a Book method.
Code
[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:
Now let’s write the minimum required code to make the test pass.
Code
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.
The next step is to refactor. We don’t have anything in the Book method to refactor but that doesn’t mean that we have nothing we can also refactor our tests.
Since we need the TicketBookingRequestProcessor in both tests, we can remove that from both tests and move it to the TicketBookingRequestProcessorTests constructor and use that in our test methods.
Code
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 = "[email protected]"
};
// 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);
}
}
Understand the second Requirement
Now we need to save the booking to the database. To save to the database we need to modify the book method to save the booking to the database and return the booking request values.
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.
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 it to the database.
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 the database and we can verify by this from the mock object that the Save method is called at least once. This is how we can use the interface to decouple the dependencies.
Now let’s create a failing Red unit test.
- Create a new test method ShouldSaveToDatabase.
- Create a request object and pass it to the processor save method.
Code
[Fact]
public void ShouldSaveToDatabase()
{
// Arrange
var request = new TicketBookingRequest
{
FirstName = "Abdul",
LastName = "Rahman",
Email = "[email protected]"
};
// Act
TicketBookingResponse response = _processor.Book(request);
}
Now to save to the database, we need ITicketBookingRepository and that needs to be injected into TicketBookingRequestProcessor.
- Add ITicketBookingRepository to TicketBookingCore project
- Add the Save() method with the TicketBooking object as a parameter.
- Press Ctrl + . to create a TicketBooking class and inherit from the TicketBookingBase class.
Code
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.
- Add _ticketBookingRepositoryMock = new Mock<ITicketBookingRepository>(); to TicketBookingRequestProcessorTests constructor.
- press Ctrl + . to download and install Moq nuget package and repeat to add a private readonly field Mock<ITicketBookingRepository> _ticketBookingRepositoryMock for that repository
- Pass the field to the TicketBookingRequestProcessor constructor and press Ctrl + . to add that as a parameter to the TicketBookingRequestProcessor class.
Code
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 the 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.
Code
[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 = "[email protected]"
};
// 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 the test setup. Run the test and the test should fail.
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 and make a call to the Save method in the repository inside the Book method in the processor class.
Code
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.
Refactor the code
Now we can improve the code by doing some refactoring.
- According to the Do Not Repeat Principle (DRY), we should avoid repeating the same code
- 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.
- 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 as follows.
Code
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 = "[email protected]"
};
_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.
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 requirements. This is a demo project and it has a lot of scope for improvement. We learned TDD with the XUnit Project, but the same can be applied to NUnit or MSTest projects as well.
Here is the link to the source code.