Test Driven Development in ASP.NET Core Web API

Test Driven Development (TDD) in ASP.NET Core Web API

In this article, I will discuss how to implement Test-Driven Development (TDD) in ASP.NET Core Web API with Step-by-Step examples. Test Driven Development (TDD) is a modern software development approach that emphasizes writing tests before developing the actual application code. By defining the desired functionality through tests first, TDD ensures that each new feature or change is clearly understood and verified as it is built. This enables developers to create reliable, well-structured code and identify errors early in the development process.

What is Test-Driven Development (TDD)?

Test-Driven Development (TDD) is a modern software development approach where tests are written before the actual code implementation. Instead of coding a feature first and then writing tests to verify it, TDD reverses this approach:

  • You start by writing tests that define the expected behaviour of your application (e.g., how API endpoints or services should respond).
  • Then, you write the minimal amount of production code needed to satisfy those tests.
  • Finally, you improve or refactor the code confidently, knowing that your tests ensure the behaviour remains correct.
The TDD Process: The Red-Green-Refactor Cycle

Test-Driven Development (TDD) is typically described as a three-step, iterative cycle that we repeat continuously as we build features. For a better understanding, please refer to the following diagram.

The TDD Process: The Red-Green-Refactor Cycle

1. Write a Failing Test (Red)

Write a unit or integration test that specifies a new feature or behaviour your API or service must have. For example, you might write a test that checks if a GET /products endpoint returns the correct list of products with an HTTP 200 status. Since you have not yet implemented this functionality, the test will fail when you run it. This failure is expected.

This phase, Red, is crucial because it:

  • Forces you to think clearly about the inputs, outputs, and expected behaviour before implementation.
  • Defines a concrete goal for your code to achieve.
  • Acts as a formal specification or contract that your code must satisfy.

Note: In ASP.NET Core Web API, these tests often use test frameworks like xUnit, NUnit, or MSTest.

2. Write Just Enough Code to Pass the Test (Green)

Make the test pass, using the simplest code possible. Write just enough logic to satisfy the requirement expressed by the test. Don’t optimize, don’t add extra features, just pass the test. For example:

  • You might implement a method in your ProductService that returns a hardcoded list of products instead of querying a real database.
  • Or a controller action that returns a fixed HTTP response matching the test’s expectations.

Note: This phase ensures you are focused on functional correctness first, establishing a working code.

3. Refactor the Code (Refactor)

Once your test passes, clean up the code without changing its external behaviour. This step involves:

  • Improving code readability and structure.
  • Removing duplicated code.
  • Renaming variables or methods for clarity.
  • Extracting reusable components or services.
  • Replacing hardcoded data with actual repository/database calls.
  • Optimizing performance where appropriate.

Note: In ASP.NET Core Web API, refactoring may involve extracting business logic from controllers into services, improving dependency injection, or enhancing error handling; your existing tests must verify all changes.

Why Is It Called Red-Green-Refactor?
  • Red: The test fails initially because the feature is not yet implemented. This defines the problem.
  • Green: After minimal coding, the test passes. This confirms that the feature or fix works as expected.
  • Refactor: You improve and clean the code without breaking the tests. This ensures long-term code quality.

Suppose you want to build an endpoint that returns a product by ID:

  • Red: Write a test that expects GET /api/products/1 to return the correct product.
  • Green: Implement the endpoint, maybe by returning a hardcoded product, so the test passes.
  • Refactor: Connect the endpoint to the real database, and ensure the test still passes.

This cycle repeats for each new feature, bug fix, or change, encouraging small, incremental progress and high code quality.

How to Apply Test-Driven Development (TDD) in ASP.NET Core Web API?

Let’s understand how to apply Test-Driven Development (TDD) in ASP.NET Core Web APIs. For a better understanding, please have a look at the following diagram:

How to Apply TDD in ASP.NET Core Web API?

In an ASP.NET Core Web API project, TDD typically involves:

Defining API Behaviour via Tests:
  • Write unit or integration tests describing how your API endpoints should behave under various conditions, valid input, invalid input, error cases, etc.
  • For example, you might write a test that calls a GET endpoint, returns the expected data with a 200 Status Code, or a POST request validates input and returns 400 on bad requests.
Implementing Minimal Code:
  • Develop the API controller methods, services, and repositories just enough to make the tests pass.
  • For instance, you might implement a mock repository to return fixed data until real database logic is added.
Confidence to Refactor and Enhance:
  • With a comprehensive list of tests, you can safely refactor controllers, business logic, and data access layers, knowing that the tests will catch any unintended side effects.
Building a Product Management API with TDD in ASP.NET Core Web API

Let’s walk through a comprehensive, real-time example of Test-Driven Development (TDD) for a simple Product Management API. We will use ASP.NET Core Web API for the backend, Entity Framework Core (Code First with SQL Server) for database management, and xUnit for unit testing. This approach will help you understand how to build robust, testable APIs by writing tests before implementing the actual features.

Project Setup

To maintain clear separation of concerns and follow best practices, we will create two projects within our solution:

  1. ProductAPI (Web API Project): This project contains all core API components, including controllers, business logic services, repository implementations, and EF Core database context. It handles HTTP requests, business logic, and data persistence.
  2. ProductAPI.Tests (xUnit Test Project): This project contains automated unit tests for the API. Following the TDD process, we write tests here before implementing features in the API. The tests target the service and controller layers using mock dependencies for isolation.
Required Packages for Testing

For effective unit testing with mock dependencies, install the Moq package in the test project. This popular mocking library helps simulate dependencies, such as repositories, during unit tests. In the Visual Studio Package Manager Console, select the ProductAPI.Tests the project and run the following command:

  • Install-Package Moq
Adding Project Reference:

Ensure that the ProductAPI.Tests project has a reference to ProductAPI. This allows the test project to access models, services, repositories, and other components from the API project.

Understanding TDD Through a Real Example: Get Product by Id

We will illustrate Test-Driven Development (TDD) by developing the ‘Get Product by ID’ feature. The development follows the classic Red-Green-Refactor cycle:

Red (Write a Failing Test First in XUnit)

Add a folder named Services to the Test Project root directory. Then, inside the Services folder, add a class file named ProductServiceTests.cs and copy and paste the following code.

using Moq;
using ProductAPI.Models;
using ProductAPI.Repositories;
using ProductAPI.Services;
namespace ProductAPI.Tests.Services
{
    public class ProductServiceTests
    {
        [Fact]
        public async Task GetProductById_ReturnsProduct_WhenProductExists()
        {
            // Arrange
            var mockRepo = new Mock<IProductRepository>();
            mockRepo.Setup(r => r.GetProductByIdAsync(1))
                .ReturnsAsync(new Product { Id = 1, Name = "Laptop", Price = 2000 });

            var service = new ProductService(mockRepo.Object);

            // Act
            var product = await service.GetProductByIdAsync(1);

            // Assert
            Assert.NotNull(product);
            Assert.Equal(1, product.Id);
            Assert.Equal("Laptop", product.Name);
        }
    }
}

At this point, the test will fail because the ProductService, Models, and related repository interfaces have not been implemented yet. This is the Red phase in TDD, where a failing test is written that defines what the system should do. This is intentional.

Green (Write Minimum Code to Pass the Test)

Now, let’s implement just enough code to make the test pass.

Define Product Model

Create a folder named Models in the project root directory of your Web API Project, and then inside the Models folder, add a class file named Product.cs and copy and paste the following code.

using System.ComponentModel.DataAnnotations.Schema;
namespace ProductAPI.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        [Column(TypeName ="decimal(12,2)")]
        public decimal Price { get; set; }
        public int Stock { get; set; }
        public string? Description { get; set; }
    }
}
Create Repository Layer

Add a folder named Repositories in the Web API Project root directory, and then inside the Repositories folder, add a class file named IProductRepository.cs and copy and paste the following code.

using ProductAPI.Models;
namespace ProductAPI.Repositories
{
    public interface IProductRepository
    {
        Task<Product?> GetProductByIdAsync(int id);
        // Add other CRUD methods as needed
    }
}
Create Service Layer

Add a folder named Services to the Web API Project root directory. 

Product Service Interface:

Create a class file named IProductService.cs within the Services folder of the Web API Project and then copy and paste the following code.

using ProductAPI.Models;
namespace ProductAPI.Services
{
    public interface IProductService
    {
        Task<Product?> GetProductByIdAsync(int id);
    }
}
Product Service Implementation:

Create a class file named ProductService.cs within the Services folder of the Web API Project and then copy and paste the following code.

using ProductAPI.Models;
using ProductAPI.Repositories;
namespace ProductAPI.Services
{
    public class ProductService : IProductService
    {
        private readonly IProductRepository _repo;

        public ProductService(IProductRepository repo)
        {
            _repo = repo;
        }

        public async Task<Product?> GetProductByIdAsync(int id)
        {
            return await _repo.GetProductByIdAsync(id);
        }
    }
}
Re-run the Test

Now that all the necessary interfaces and classes are in place, run your test again. It should pass (Green), confirming that your minimal implementation meets the test expectations.

Refactor (Improve Implementation)

Now that we have a working feature, improve the codebase and integrate a real data database. To integrate with a real database, we need to set up Entity Framework Core (EF Core) and implement the repository.

EF Core Setup:

Please execute the following commands in Visual Studio Package Manager Console. Please ensure to select the Web API Project.

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Define DbContext

Create a folder named Data in the Web API project root directory, and then inside the Data folder, add a class file named ProductDbContext.cs and copy and paste the following code.

using ProductAPI.Models;
using Microsoft.EntityFrameworkCore;
namespace ProductAPI.Data
{
    public class ProductDbContext : DbContext
    {
        public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options) { }

        public DbSet<Product> Products { get; set; } = null!;
    }
}
Product Repository Implementation

Create a class file named ProductRepository.cs within the Repositories folder of the Web API Project and then copy and paste the following code.

using Microsoft.EntityFrameworkCore;
using ProductAPI.Data;
using ProductAPI.Models;
namespace ProductAPI.Repositories
{
    public class ProductRepository : IProductRepository
    {
        private readonly ProductDbContext _context;

        public ProductRepository(ProductDbContext context)
        {
            _context = context;
        }

        public async Task<Product?> GetProductByIdAsync(int id)
        {
            return await _context.Products.FirstOrDefaultAsync(p => p.Id == id);
        }
    }
}
Register Services in DI Container

ASP.NET Core uses built-in Dependency Injection (DI) to manage class dependencies.

  • Register your repository and service implementations with the dependency injection (DI) container.
  • This allows your controllers and services to request dependencies via constructor injection.
  • Using scoped lifetimes ensures that there is one instance per HTTP request, making them suitable for API services.

This setup keeps your app modular, testable, and adheres to best practices. So, please modify the Program class of your Web API Project as follows.

using Microsoft.EntityFrameworkCore;
using ProductAPI.Data;
using ProductAPI.Repositories;
using ProductAPI.Services;
namespace ProductAPI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllers()
                            .AddJsonOptions(options =>
                            {
                                //Disable Camel Case
                                options.JsonSerializerOptions.PropertyNamingPolicy = null;
                            });

            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // EF Core DbContext
            builder.Services.AddDbContext<ProductDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

            // Dependency Injection
            builder.Services.AddScoped<IProductRepository, ProductRepository>();
            builder.Services.AddScoped<IProductService, ProductService>();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
Connection String Add to appsettings.json

Please modify the appsettings.json file as follows:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ProductManagementDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Controller Layer:

Create an API Empty Controller named ProductsController within the Controllers folder of your Web API project, and then copy and paste the following code.

using Microsoft.AspNetCore.Mvc;
using ProductAPI.Services;
namespace ProductAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly IProductService _service;
        public ProductsController(IProductService service)
        {
            _service = service;
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> GetById(int id)
        {
            var product = await _service.GetProductByIdAsync(id);
            if (product == null)
                return NotFound();
            return Ok(product);
        }
    }
}
EF Core Code-First Migration

In Visual Studio, open the Package Manager Console and execute the following commands: Add-Migration and Update-Database. This will generate the Migration file, which is then applied to create the ProductManagementDB database and its required tables. While executing this command, please select the ProductAPI project.

Test-Driven Development in ASP.NET Core Web API

Once you execute the above commands, verify the database, and you should see the ProductManagementDB database with the required Product table as shown in the image below.

Test-Driven Development (TDD) in ASP.NET Core Web API

Now, both your application and unit test should work as expected for the Get Product by ID feature.

Add More TDD Scenarios

Continue applying TDD for all new features:

  • Write tests for listing all products, adding, updating, and deleting products.
  • For each new feature, start with a failing test, implement the minimal code necessary to pass, and then refactor and improve.
Benefits of TDD in ASP.NET Core Web API

Using TDD in building ASP.NET Core Web APIs offers several important benefits:

  • Clear Requirements: Writing tests first forces you to think about the exact behaviour and requirements before coding.
  • Better Design: Producing minimal code to pass tests encourages simpler, more modular design.
  • Early Bug Detection: Since tests are written upfront and run frequently, bugs are caught early.
  • Regression Safety Net: Tests ensure that changes or refactoring don’t break existing functionality.
  • Documentation: Tests serve as living documentation for the expected behaviour of your application.
  • Better Collaboration Between Teams: Backend and frontend teams can agree on the API contract through tests. Tests define clear input-output behaviour, reducing ambiguity.

In summary, Test-Driven Development helps create high-quality software by enforcing a disciplined process of writing tests first, then implementing code to pass those tests, followed by continuous refactoring. This cycle promotes better design, early detection of errors, and confidence that changes won’t break existing functionality, leading to more robust and maintainable applications.

In the next article, I will discuss the xUnit Framework Theory Attribute in ASP.NET Core. In this article, I explain how to implement Test-Driven Development (TDD) in an ASP.NET Core Web API. I hope you enjoy this article on Test-Driven Development (TDD) in an ASP.NET Core Web API.

Leave a Reply

Your email address will not be published. Required fields are marked *