Test-Driven Development (TDD) in .NET Core
Test-Driven Development (TDD) is a software development process where tests are written before the actual code. TDD is popular because it helps improve code quality, provides immediate feedback to developers, and encourages better design. It consists of a short, repetitive cycle: Red, Green, and Refactor.
Here's how TDD works in practice, especially in a .NET Core context:
1️⃣ The TDD Cycle
The TDD cycle involves three basic steps, repeated iteratively:
1. Red: Write a Failing Test
Objective: Start by writing a test for a new feature or bug fix.
Result: The test should fail because the feature isn’t implemented yet.
Example: If you're adding a new method to your Calculator class, you write a test to check that method’s behavior.
public class CalculatorTests
{
[Fact]
public void Add_TwoNumbers_ReturnsCorrectSum()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.Equal(5, result);
}
}
2. Green: Make the Test Pass
Objective: Write the minimal code necessary to make the test pass.
Result: Once you write just enough code, the test should pass.
Example: Implement the Add method in the Calculator class so that the test passes.
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
Now, the test will pass because the Add method works as expected.
3. Refactor: Improve the Code
Objective: Refactor the code without changing its behavior.
Result: Clean up any redundancies, improve the design, or optimize the code.
Example: In this simple case, the code is already minimal, but in a more complex example, you might refactor for readability, performance, or maintainability.
2️⃣ Setting Up TDD in .NET Core
1. Create a New .NET Core Project
You can set up TDD in a new or existing .NET Core project by creating a Unit Test Project.
Create an ASP.NET Core Web API:
dotnet new webapi -n TDDExample
Add a Test Project:
dotnet new xunit -n TDDExample.Tests
Add the Test Project to Solution:
dotnet sln add TDDExample.Tests/TDDExample.Tests.csproj
Add a Reference to the API Project:
dotnet add TDDExample.Tests reference TDDExample
2. Install Required NuGet Packages
xUnit for writing tests.
Moq or NSubstitute for mocking dependencies (if needed).
FluentAssertions for more readable assertions (optional).
dotnet add package xunit
dotnet add package Moq
dotnet add package FluentAssertions
3. Write Your First Test
Once the test project is set up, you can begin following the TDD cycle.
For example, let’s say you’re building a UserService to fetch user data from a database.
Step 1: Write a Failing Test
public class UserServiceTests
{
[Fact]
public void GetUserById_ReturnsUser_WhenUserExists()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(repo => repo.GetUserById(1)).Returns(new User { Id = 1, Name = "John Doe" });
var userService = new UserService(mockRepo.Object);
// Act
var result = userService.GetUserById(1);
// Assert
Assert.Equal(1, result.Id);
Assert.Equal("John Doe", result.Name);
}
}
Step 2: Implement the Code to Make the Test Pass
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public User GetUserById(int id)
{
return _userRepository.GetUserById(id);
}
}
Step 3: Refactor the Code
After the test passes, you might notice that you can refactor the method or even improve your mock setup, but the core logic should remain unchanged.
For example:
public User GetUserById(int id)
{
var user = _userRepository.GetUserById(id);
return user ?? throw new Exception("User not found");
}
3️⃣ Best Practices for TDD in .NET Core
Write Simple Tests First: Start by writing simple, straightforward tests that directly relate to the behavior you're implementing.
Test One Thing at a Time: Each test should focus on one behavior, whether it’s a simple calculation or a feature of your service.
Small, Frequent Commits: Make frequent, small commits to maintain a steady pace. This allows you to isolate errors and maintain high code quality.
Refactor with Caution: After tests pass, refactor code, but be mindful of maintaining test coverage for new changes.
Test Coverage & Edge Cases: Aim to cover edge cases, such as handling nulls, exceptions, or special data. Don’t just test the “happy path.”
Avoid Over-Mocking: Mock external services or repositories but try to avoid mocking business logic or simple methods. This keeps your tests meaningful.
Use Dependency Injection: Leverage dependency injection to make it easy to replace real dependencies with mocks during testing. .NET Core's built-in DI system makes this easy.
4️⃣ Example of TDD for a Full Use Case
Let’s walk through TDD for a small application where we build a ProductService that interacts with a ProductRepository. The ProductService will have a method that returns a list of products by category.
Step 1: Write a Failing Test
public class ProductServiceTests
{
[Fact]
public void GetProductsByCategory_ReturnsCorrectProducts()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
var products = new List<Product>
{
new Product { Id = 1, Name = "Product A", Category = "Electronics" },
new Product { Id = 2, Name = "Product B", Category = "Electronics" }
};
mockRepo.Setup(repo => repo.GetProductsByCategory("Electronics")).Returns(products);
var productService = new ProductService(mockRepo.Object);
// Act
var result = productService.GetProductsByCategory("Electronics");
// Assert
Assert.Equal(2, result.Count());
Assert.Contains(result, p => p.Name == "Product A");
}
}
Step 2: Make the Test Pass
public class ProductService
{
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public IEnumerable<Product> GetProductsByCategory(string category)
{
return _productRepository.GetProductsByCategory(category);
}
}
Step 3: Refactor
Refactor the code to optimize the logic or extract the method into smaller parts.
public IEnumerable<Product> GetProductsByCategory(string category)
{
var products = _productRepository.GetProductsByCategory(category);
return products.Where(p => p.Category == category);
}
5️⃣ Benefits of TDD in .NET Core
Improved Code Quality: TDD encourages you to think about the code’s design and behavior from the very beginning.
Immediate Feedback: You get instant feedback after each code change, allowing quick identification of bugs.
Maintainability: Since tests ensure that your code behaves as expected, refactoring becomes safer and easier.
Comprehensive Documentation: Tests serve as documentation for how the code is supposed to work.
Confidence in Changes: You can confidently change or extend your code, knowing that tests will catch regressions.
6️⃣ Conclusion
Test-Driven Development in .NET Core is a powerful technique for writing clean, maintainable, and bug-free code. The Red-Green-Refactor cycle helps developers focus on writing tests before code and ensures that each part of the application is thoroughly tested.
Red: Write the failing test.
Green: Make it pass.
Refactor: Improve the code without changing functionality.
By following TDD, you ensure that your application behaves as expected and is easier to maintain and extend over time.
Learn Dot Net Course in Hyderabad
Read More
Mocking and Stubbing in Unit Testing for .NET Core
Writing Automated UI Tests for Full Stack .NET Applications
Testing Web APIs in .NET with Postman and xUnit
Testing Web APIs in .NET with Postman and xUnit
Visit Our Quality Thought Institute in Hyderabad
Subscribe by Email
Follow Updates Articles from This Blog via Email
No Comments