- Home
- .NET tutorials
- How to add unit testing to Minimal APIs routes using xUnit
How to add unit testing to Minimal APIs routes using xUnit
Published: Monday 15 September 2025
I wanted to unit test my Minimal API endpoints. But then I came across a problem. How do I do it?
My current state of tests
I currently had all my endpoints contained within Program.cs
with the endpoint handlers using lambda expressions:
// ToDoService.cs
public interface IToDoService
{
void Create(CreateToDoItemDto createToDoItem);
ToDoItemDto? Get(int id);
void Update(int id, UpdateToDoItemDto updateToDoItem);
void Delete(int id);
}
// ToDoService.cs
public class ToDoService : IToDoService
{
private int Seed { get; set; }
private IList<ToDoItemDto> Items { get; }
public ToDoService()
{
Seed = 1;
Items = new List<ToDoItemDto>();
}
public void Create(CreateToDoItemDto createToDoItem)
{
Items.Add(new(Seed, createToDoItem.Text));
Seed++;
}
public ToDoItemDto? Get(int id)
{
return Items.SingleOrDefault(i => i.Id == id);
}
public void Update(int id, UpdateToDoItemDto updateToDoItem)
{
if (!Items.Any(i => i.Id == id))
{
return;
}
var item = Items.Single(i => i.Id == id);
item.UpdateText(updateToDoItem.Text);
}
public void Delete(int id)
{
if (!Items.Any(i => i.Id == id))
{
return;
}
Items.Remove(Items.Single(i => i.Id == id));
}
}
// Program.cs
app.MapGet("/todo/{id}",
Results<Ok<ToDoItemDto>, NotFound>
(int id, IToDoService toDoService) =>
{
var item = toDoService.Get(id);
if (item == null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(item);
});
app.MapPost("/todo", (CreateToDoItemDto createItem, IToDoService toDoService) =>
{
toDoService.Create(createItem);
return TypedResults.NoContent();
});
app.MapPut("/todo/{id}", (int id, UpdateToDoItemDto updateItem, IToDoService toDoService) =>
{
toDoService.Update(id, updateItem);
return TypedResults.NoContent();
});
app.MapDelete("/todo/{id}", (int id, IToDoService toDoService) =>
{
toDoService.Delete(id);
return TypedResults.NoContent();
});
I couldn't see a way of being able to test what was inside each endpoint.
Then I had a brainwave! I could put each endpoint handler into its own separate method and then unit test that. Bingo!
Move to an Endpoints class
But before that, I wanted to move the endpoints themselves from Program.cs
into a separate class. This step is optional, but it really isn't as your Program.cs
file will get too bloated.
// ToDoEndpoints.cs
public static class ToDoEndpoints
{
public static void MapToDoGroup(this WebApplication app)
{
var toDoGroup = app.MapGroup("/todo");
toDoGroup.MapGet("/{id}",
Results<Ok<ToDoItemDto>, NotFound>
(int id, IToDoService toDoService) =>
{
var item = toDoService.Get(id);
if (item == null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(item);
});
toDoGroup.MapPost("/", (CreateToDoItemDto createItem, IToDoService toDoService) =>
{
toDoService.Create(createItem);
return TypedResults.NoContent();
});
toDoGroup.MapPut("/{id}", (int id, UpdateToDoItemDto updateItem, IToDoService toDoService) =>
{
toDoService.Update(id, updateItem);
return TypedResults.NoContent();
});
toDoGroup.MapDelete("/{id}", (int id, IToDoService toDoService) =>
{
toDoService.Delete(id);
return TypedResults.NoContent();
});
}
}
You then make a call to the MapToDoGroup()
in your Program.cs
file:
// Program.cs
app.MapToDoGroup();
Extract methods in different methods
Within the same class, I extracted the endpoint handlers from the routes and put them into their own separate methods:
// ToDoEndpoints.cs
public static class ToDoEndpoints
{
public static void MapToDoGroup(this WebApplication app)
{
var toDoGroup = app.MapGroup("/todo");
toDoGroup.MapGet("/{id}", (int id, IToDoService toDoService) =>
Get(id, toDoService));
toDoGroup.MapPost("/", (CreateToDoItemDto createItem, IToDoService toDoService) =>
Create(createItem, toDoService));
toDoGroup.MapPut("/{id}", (int id, UpdateToDoItemDto updateItem, IToDoService toDoService) =>
Update(id, updateItem, toDoService));
toDoGroup.MapDelete("/{id}", (int id, IToDoService toDoService) =>
Delete(id, toDoService));
}
public static Results<Ok<ToDoItemDto>, NotFound> Get(int id, IToDoService toDoService)
{
var item = toDoService.Get(id);
if (item == null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(item);
}
public static NoContent Create(CreateToDoItemDto createItem, IToDoService toDoService)
{
toDoService.Create(createItem);
return TypedResults.NoContent();
}
public static NoContent Update(
int id,
UpdateToDoItemDto updateItem,
IToDoService toDoService)
{
toDoService.Update(id, updateItem);
return TypedResults.NoContent();
}
public static NoContent Delete(int id, IToDoService toDoService)
{
toDoService.Delete(id);
return TypedResults.NoContent();
}
}
Bingo! I was now able to test the endpoint handlers by calling those individual methods.
Setting up the xUnit project
I set up the xUnit project and added the Moq NuGet package to allow me to mock an implementation of IToDoService
.
I had to add the Web API project as a dependency to my xUnit test so I was able to test the extension methods that were included in the ToDoEndpoints
class.
Then it was time to create the test class. I wanted a mock instance of the IToDoService
interface to be available for all tests. I created an instance of it in the constructor and stored it in a private readonly
method in the class.
// ToDoEndpointsTests.cs
public class ToDoEndpointsTests
{
private readonly Mock<IToDoService> _toDoServiceMock;
public ToDoEndpointsTests()
{
_toDoServiceMock = new Mock<IToDoService>();
}
}
Time to write my tests. With the Get
method, I wanted to make sure that I tested if a result was found or not. As I was using TypedResults
to return my responses, I could check to see if the type returned was a NotFound
type or an Ok
type:
// ToDoEndpointsTests.cs
public class ToDoEndpointsTests
{
private readonly Mock<IToDoService> _toDoServiceMock;
public ToDoEndpointsTests()
{
_toDoServiceMock = new Mock<IToDoService>();
}
[Fact]
public void Get_RecordNotExists_ReturnsNotFound()
{
// Act
var act = ToDoEndpoints.Get(1, _toDoServiceMock.Object);
// Assert
Assert.IsType<NotFound>(act.Result);
}
[Fact]
public void Get_RecordExists_ReturnsOkWithDto()
{
// Arrange
var toDoItem = new ToDoItemDto(1, "This is text");
_toDoServiceMock.Setup(s => s.Get(1))
.Returns(toDoItem);
// Act
var act = ToDoEndpoints.Get(1, _toDoServiceMock.Object);
// Assert
Assert.IsType<Ok<ToDoItemDto>>(act.Result);
var result = act.Result as Ok<ToDoItemDto>;
Assert.Equal(toDoItem, result!.Value);
}
}
On to the Create
method. I wanted to verify that the Create
method in the IToDoService
type was called once with the expected values.
// ToDoEndpointsTests.cs
public class ToDoEndpointsTests
{
...
[Fact]
public void Create_WhenCalled_CreatesToDo()
{
// Arrange
var createToDoItem = new CreateToDoItemDto("This is text");
// Act
var act = ToDoEndpoints.Create(createToDoItem, _toDoServiceMock.Object);
// Assert
Assert.IsType<NoContent>(act);
_toDoServiceMock.Verify(s => s.Create(createToDoItem), Times.Once);
}
}
And it was a similar story with the Update
and Delete
methods. I needed to verify that they called the relevant methods inside the IToDoService
type:
public class ToDoEndpointsTests
{
...
[Fact]
public void Update_WhenCalled_UpdatesToDo()
{
// Arrange
var id = 1;
var updateToDo = new UpdateToDoItemDto("Update to this");
// Act
var act = ToDoEndpoints.Update(id, updateToDo, _toDoServiceMock.Object);
// Assert
Assert.IsType<NoContent>(act);
_toDoServiceMock.Verify(s => s.Update(id, updateToDo), Times.Once);
}
[Fact]
public void Delete_WhenCalled_DeletesToDo()
{
// Arrange
var id = 1;
// Act
var act = ToDoEndpoints.Delete(id, _toDoServiceMock.Object);
// Assert
Assert.IsType<NoContent>(act);
_toDoServiceMock.Verify(s => s.Delete(id), Times.Once);
}
}
Here is the full code:
// ToDoEndpointsTests.cs
public class ToDoEndpointsTests
{
private readonly Mock<IToDoService> _toDoServiceMock;
public ToDoEndpointsTests()
{
_toDoServiceMock = new Mock<IToDoService>();
}
[Fact]
public void Get_RecordNotExists_ReturnsNotFound()
{
// Act
var act = ToDoEndpoints.Get(1, _toDoServiceMock.Object);
// Assert
Assert.IsType<NotFound>(act.Result);
}
[Fact]
public void Get_RecordExists_ReturnsOkWithDto()
{
// Arrange
var toDoItem = new ToDoItemDto(1, "This is text");
_toDoServiceMock.Setup(s => s.Get(1))
.Returns(toDoItem);
// Act
var act = ToDoEndpoints.Get(1, _toDoServiceMock.Object);
// Assert
Assert.IsType<Ok<ToDoItemDto>>(act.Result);
var result = act.Result as Ok<ToDoItemDto>;
Assert.Equal(toDoItem, result!.Value);
}
[Fact]
public void Create_WhenCalled_CreatesToDo()
{
// Arrange
var createToDoItem = new CreateToDoItemDto("This is text");
// Act
var act = ToDoEndpoints.Create(createToDoItem, _toDoServiceMock.Object);
// Assert
Assert.IsType<NoContent>(act);
_toDoServiceMock.Verify(s => s.Create(createToDoItem), Times.Once);
}
[Fact]
public void Update_WhenCalled_UpdatesToDo()
{
// Arrange
var id = 1;
var updateToDo = new UpdateToDoItemDto("Update to this");
// Act
var act = ToDoEndpoints.Update(id, updateToDo, _toDoServiceMock.Object);
// Assert
Assert.IsType<NoContent>(act);
_toDoServiceMock.Verify(s => s.Update(id, updateToDo), Times.Once);
}
[Fact]
public void Delete_WhenCalled_DeletesToDo()
{
// Arrange
var id = 1;
// Act
var act = ToDoEndpoints.Delete(id, _toDoServiceMock.Object);
// Assert
Assert.IsType<NoContent>(act);
_toDoServiceMock.Verify(s => s.Delete(id), Times.Once);
}
}
Watch the video
Watch the video to find out how you can add xUnit tests to your Minimal API routes:
Download the code example
And when you watch the video, you can download the code example and follow along without having to type any code.
Final thoughts
Yes there is a bit more work to do when unit testing your Minimal API endpoints. You have to make sure that your endpoint handlers are moved to methods so you are able to call them in your unit tests. But it goes to show that you have no issues in unit testing your endpoint handlers.
Related tutorials
