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.