Unit Testing Mininal APIs

Unit Testing Mininal APIs

Introduction

With the advent of .NET 6, Minimal APIs have become a popular way to build lightweight, efficient web services in C#.

However, the simplicity of Minimal APIs also raises questions about best practices for unit testing.

This article explores strategies and best practices for effectively unit testing C# Minimal APIs.

Understanding Minimal APIs

Minimal APIs in .NET 8 streamline the process of setting up a web service by reducing boilerplate code. They are defined in the Program.cs file, combining startup and routing configurations in one place.

While this is convenient for development, it poses unique challenges for unit testing.

Challenges in Unit Testing Minimal APIs

  1. Lack of Controllers: Unlike traditional MVC controllers, Minimal APIs handle requests directly in the Program.cs, making them less straightforward to isolate for testing.
  2. Shared Context: The application’s configuration and services are often set up in the same file as the endpoints, complicating the separation of concerns.

Strategies for Unit Testing Minimal APIs

  1. Refactor Logic into Services:
    • Extract the business logic handled by your endpoints into separate service classes.
    • This makes it easier to test the logic in isolation and promotes separation of concerns.
  2. Mock Dependencies:
    • Use mocking frameworks like Moq to mock dependencies passed to your services.
    • This allows you to test how your service behaves under different conditions without relying on real implementations of dependencies.
  3. Integration Testing:
    • For testing the actual endpoints, consider writing integration tests.
    • Use the WebApplicationFactory or TestServer to create a test server and send HTTP requests to your endpoints.
    • This helps test the routing and integration with other parts of your application.

Example

Consider an endpoint that adds a new customer:

app.MapPost("/customers"
  async (CustomerDto customer, ICustomerService service) => 
  {
      await service.AddCustomer(customer); 
      
      return Results.Ok(); 
  });

The business logic for adding a customer is in ICustomerService. To unit test this logic:

Create a Mock for ICustomerService:

var mockServicenew Mock<ICustomerService>(); 
mockService.Setup(service => 
    service.AddCustomer(It.IsAny<CustomerDto>())) 
    .Returns(Task.CompletedTask);

Write Unit Test for the Service:

[Fact
public async Task AddCustomer_ShouldCallService() 

    var customerDtonewCustomerDto/* ... properties ... */ }; 
    
    await mockService.Object.AddCustomer(customerDto); 
    mockService.Verify(service => 
          service.AddCustomer(customerDto), Times.Once()); 
    }

For integration testing, use WebApplicationFactory to create a client and send a request to the /customers endpoint, asserting the response.

Best Practices

  • Keep Business Logic Out of Endpoints: Endpoints should handle routing and minimal logic. Offload business logic to services for easier testing.
  • Use Dependency Injection: Leverage dependency injection to pass services and configurations. This makes it easier to swap real implementations with mocks during testing.
  • Write Meaningful Tests: Focus on scenarios that provide value, such as edge cases, common use cases, and error handling paths.

Conclusion

While unit testing C# Minimal APIs presents unique challenges, proper structuring of your application and reliance on service classes can significantly ease the process.

Separating business logic from routing logic not only facilitates easier unit testing but also leads to a cleaner, more maintainable codebase.

Integration testing complements unit testing by ensuring the whole system, including routing and middleware, works as expected.

Stephen

Hi, my name is Stephen Finchett. I have been a software engineer for over 30 years and worked on complex, business critical, multi-user systems for all of my career. For the last 15 years, I have been concentrating on web based solutions using the Microsoft Stack including ASP.Net, C#, TypeScript, SQL Server and running everything at scale within Kubernetes.