Implementing TDD in C# .Net
TDD
2 Articles
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
- Scenario
- Using TDD to write business logic
- Understanding the First Requirement
- Create a Red unit test
- Write Code to make test Green
- Refactor to improve the code
- Understanding the second Requirement
- Decouple the Dependencies
- Writing code to make the test pass
- Again refactor the code
- 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
- Response should contain the same values as request after booking.
- 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.
This simple requirement is good to start with TDD.
Create a Red unit test
- Create a new
C# XUnit
test project namedTicketBookingCore.Tests
. -
As you need to test
TicketBookingProcessor class
, create a new class namedTicketBookingRequestProcessorTests
. - 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
NamedTicketBookingCore
and moveTicketBookingRequest
,TicketBookingRequestProcessor
andTicketBookingResponse
into this project. - Fix
Namespaces
of the files inTicketBookingCore
project. - Change the
access modifier
of all classes and methods to thepublic
. -
Create a
base class TicketBookingBase
and move the properties fromTicketBookingRequest
andTicketBookingResponse
andinherit
fromTicketBookingBase class
. -
Add a
reference
toTicketBookingCore
project in theTicketBookingCore.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
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.
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
toTicketBookingCore
project - Add
Save() method
withTicketBooking object
as a parameter. -
Press Ctrl + . to create a
TicketBooking class
andinherit
from theTicketBookingBase 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>();
toTicketBookingRequestProcessorTests constructor
. -
Press Ctrl + . to download install
Moq
nuget package and repeat to add a private readonly fieldMock<ITicketBookingRepository> _ticketBookingRepositoryMock
for that repository. -
Pass the field to
TicketBookingRequestProcessor constructor
and press Ctrl + . to add that as a parameter toTicketBookingRequestProcessor 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.
Again 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 sameTicketBookingRequest object
is constructed and used in two methods. This can be moved to theTicketBookingRequestProcessorTests constructor
and can be made as aprivate 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.
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.