In our previous article Introducing TDD in C# .NET,
we learnt what is the principle of TDD, it's advantages and disadvantages. We also learnt the terms
Red, Green, Refactor
in TDD. In this article, let's learn about how to practice and implement TDD in C# .NET.
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)
- 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
- Response should contain the same values as request after booking.
- Booking should be saved to 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 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.
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 [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 Sample - TDD Red Phase - First Requirement - New Unit Test
Create a TicketBookingRequest with FirstName, LastName and Email properties and set the values and again press Ctrl + . to create the class in new file.
Code Sample - TDD Red Phase - First Requirement - Creating Request Class
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.
Code Sample - TDD Red Phase - First Requirement - Creating Response Class
We are all set with AAA (Arrange, Act, and Assert) of unit tests.
Code Sample - TDD Red Phase - First Requriement - Arrange Assert Act
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 Book Method of TicketBookingRequestProcessor class.
Code Sample - TDD Green Phase - First Requirement - Adding Minimal Requried Code
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 New .Net Core Class Library Named TicketBookingCore and move TicketBookingRequest, TicketBookingRequestProcessor and TicketBookingResponse into this project.
- Fix Namespaces of the files in 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 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 request is not null while calling a Book method.
Code Sample - TDD Red Phase - First Requirement - Another Unit Test
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 Sample - TDD Green Phase - First Requriement - Minimal Required Code to Make Test Pass
Now run the test again and the test will pass.
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.
Code Sample - TDD Refactor Phase - First Requriement - Refactoring Tests
Understand 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.
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 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 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,
- Create a new test method ShouldSaveToDatabase.
- Create a request object and pass it to the processor save method.
Code Sample - TDD Red Phase - Second Requirement - New Failing Test
Now to save to database, we need ITicketBookingRepository and that needs to be injected to TicketBookingRequestProcessor.
- Add ITicketBookingRepository to TicketBookingCore project
- Add Save() method with TicketBooking object as a parameter.
- Press Ctrl + . to create a TicketBooking class and inherit from the TicketBookingBase class.
Code Sample - TDD Red Phase - Second Requirement - New Failing Test Compilation Fix
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 install Moq nuget package and repeat to add a private readonly field Mock<ITicketBookingRepository> _ticketBookingRepositoryMock for that repository.
- Pass the field to TicketBookingRequestProcessor constructor and press Ctrl + . to add that as a parameter to TicketBookingRequestProcessor class.
Code Sample - TDD Red Phase - Second Requirement - New Failing Test Mocking Dependencies
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.
Code Sample - TDD Red Phase - Second Requirement - New Failing Test Asserting
Now we are done with 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 make a call to Save method in repository inside the Book method in processor class.
Code Sample - TDD Green Phase - Second Requirement - Minimal Requried Code
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 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 like as follows:
Code Sample - TDD Complete Code
Now run all the tests. All the tests should pass.
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.