Updated on
An integration test in ASP.NET Core starts the real application in memory with WebApplicationFactory<Program>, sends it HTTP requests and checks the responses, so it shows us that the pieces fit together.
We’ll write four of those tests for the EmployeesApp MVC application from this series. Additionally, we will prepare an in-memory database so we don’t have to use the real SQL server during integration tests.
Is your app built on minimal APIs instead? Everything we do with the factory works the same way there.
To navigate this series, you can visit ASP.NET Core Testing.
Let’s move on.
What Is Integration Testing?
Integration testing ensures that different components inside the application function correctly when working together.
The main difference between integration testing and unit testing is that integration testing often includes an application’s infrastructure components like a database, file system, etc. When we work with unit tests, we mock these components. But with integration testing, we want to ensure that the whole app is working as expected with all of these components combined.
The difference shows as soon as two pieces are wired together wrong. Delete the line in Program.cs that registers our employee repository, and all 17 unit tests in the sample still pass. The four integration tests we write in this article fail with a 500 status code.
Integration tests are slower, though. In our runs, the four integration tests took 0.9 to 4 seconds, the 17 unit tests under one. So we’ll want them for the paths that matter most, and we still replace the parts we don’t control, such as the database.
For a third-party HTTP API, the stand-in is a fake server such as WireMock.NET.
And if the registrations are all you want to check, a lighter test can resolve the IServiceCollection registrations without starting the app at all.
Preparing a New Project for Integration Testing
So, let’s see how we can write integration tests in ASP.NET Core.
First, we are going to create a new xUnit project named EmployeesApp.IntegrationTests for integration testing purposes. Additionally, we are going to add it to the solution, reference the main project and install two NuGet packages required for testing purposes.
From the folder that holds EmployeesApp.sln, let’s run:
dotnet new xunit -n EmployeesApp.IntegrationTests dotnet sln EmployeesApp.sln add EmployeesApp.IntegrationTests/EmployeesApp.IntegrationTests.csproj dotnet reference add EmployeesApp/EmployeesApp.csproj --project EmployeesApp.IntegrationTests dotnet package add Microsoft.AspNetCore.Mvc.Testing --project EmployeesApp.IntegrationTests dotnet package add Microsoft.EntityFrameworkCore.InMemory --project EmployeesApp.IntegrationTests
The two packages have different jobs:
Microsoft.AspNetCore.Mvc.Testing: this package provides theTestServerand an important class,WebApplicationFactory, to help us bootstrap our app in memory. It also points the content root at ourEmployeesAppproject, so the app reads its ownappsettings.json.Microsoft.EntityFrameworkCore.InMemory: the in-memory database provider that stands in for SQL Server.
After the project creation, we are going to rename the UnitTest1.cs class to EmployeesControllerIntegrationTests.
If xUnit itself is new to you, we covered writing xUnit tests in the first part of this series.
Now we can continue on.
What Does WebApplicationFactory Do?
The WebApplicationFactory<TEntryPoint> class is a factory we can use to bootstrap an application in memory for functional end-to-end tests. It comes from the Microsoft.AspNetCore.Mvc.Testing package, and TEntryPoint is a class from the app, normally Program.
The factory runs the app’s own Program.cs and hosts the result on TestServer, an in-memory server with no port and no network.
The app starts the first time we ask for a client, and only once per factory. CreateClient() returns an HttpClient whose requests go straight into the app’s middleware pipeline.
By default, that client follows redirects and keeps cookies, like a browser.
To change the app for our tests, we derive from the factory and override ConfigureWebHost(). The services we configure there are applied after the ones in Program.cs.
So we can remove a service the app registered and add a test version, which is how we swap SQL Server for the in-memory database.
None of this depends on MVC. The factory works the same way with minimal APIs, because our tests only see HTTP.
The diagram below shows the path each request takes in our tests. Start with the test at the top, and you’ll notice that only TestServer and the last box are different from the app as it normally runs.

So, let’s see how we can use the WebApplicationFactory class to create an in-memory factory configuration.
The first thing we are going to do is to create a TestingWebAppFactory.cs file in the EmployeesApp.IntegrationTests project, with a first version of our factory:
public class TestingWebAppFactory<TEntryPoint> : WebApplicationFactory<Program> where TEntryPoint : Program
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
}
}
For this to work, we need two namespaces included:
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing;
Our class derives from WebApplicationFactory<Program> and overrides the ConfigureWebHost method, which lets us configure the application before it’s built.
Where does Program come from? Our Program.cs uses top-level statements, so the compiler generates the Program class behind the scenes.
Before .NET 10, the compiler generated it as an internal class, thus making it inaccessible in our integration testing project. The build failed with error CS0122: 'Program' is inaccessible due to its protection level, and to solve this, we had to add public partial class Program { } at the end of Program.cs.
On .NET 10, we can skip that step. Microsoft’s release notes for ASP.NET Core in .NET 10 explain why: “In .NET 10, a source generator is used to generate the public partial class Program declaration if the programmer didn’t declare it explicitly.”
If your Program.cs already ends with that line, it does no harm, and Visual Studio suggests deleting it (analyzer ASP0027).
Implementation of the ConfigureWebHost Method
Now, we can implement the ConfigureWebHost method. Let’s replace the contents of TestingWebAppFactory.cs with the complete class:
using EmployeesApp.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Linq;
namespace EmployeesApp.IntegrationTests
{
public class TestingWebAppFactory<TEntryPoint> : WebApplicationFactory<Program> where TEntryPoint : Program
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.RemoveAll<DbContextOptions<EmployeeContext>>();
services.RemoveAll<IDbContextOptionsConfiguration<EmployeeContext>>();
services.AddDbContext<EmployeeContext>(options =>
{
options.UseInMemoryDatabase("InMemoryEmployeeTest");
});
var sp = services.BuildServiceProvider();
using (var scope = sp.CreateScope())
using (var appContext = scope.ServiceProvider.GetRequiredService<EmployeeContext>())
{
try
{
appContext.Database.EnsureCreated();
}
catch (Exception ex)
{
//Log errors or do anything you think it's needed
throw;
}
}
});
}
}
}
A couple of things to mention here.
In the ConfigureWebHost method, we remove the SQL Server setup that Program.cs registered for EmployeeContext. Why two RemoveAll() calls?
Since EF Core 9, AddDbContext() registers our UseSqlServer() call as a separate IDbContextOptionsConfiguration<EmployeeContext> service, next to the DbContextOptions<EmployeeContext> it has always registered.
If we remove only the options, the SQL Server setup survives and EF Core finds two providers. Every test then fails before it sends a request:
System.InvalidOperationException : Services for database providers 'Microsoft.EntityFrameworkCore.SqlServer', 'Microsoft.EntityFrameworkCore.InMemory' have been registered in the service provider. Only a single database provider can be registered in a service provider.
After that, we configure the database context again and instruct it to use the in-memory database instead of the real database.
Finally, with EnsureCreated(), we ensure that we seed the data from the EmployeeContext class, the two employees it adds with HasData(). It’s the same data you inserted into a real SQL Server database at the beginning of this series.
The in-memory provider isn’t a relational database, though. It can’t run migrations, raw SQL or transactions, and some queries behave differently than they do on SQL Server. EF Core’s documentation on the provider says: “While some users use the in-memory database for testing, this is discouraged.”
So why use it here? Our tests don’t need SQL. They check routing, validation, views and the DI registrations, which is what this series is about.
When your test has to prove the SQL side too, run it against a real SQL Server in a Docker container with Testcontainers.
With these preparations in place, we can return to the test class and start writing our tests.
Integration Testing of the Index Action
In our test class, we can find a single test method with the default name. But let’s remove it and start from scratch.
The first thing we have to do is to use a previously created TestingWebAppFactory class:
public class EmployeesControllerIntegrationTests : IClassFixture<TestingWebAppFactory<Program>>
{
private readonly HttpClient _client;
public EmployeesControllerIntegrationTests(TestingWebAppFactory<Program> factory)
=> _client = factory.CreateClient();
}
So, we implement the IClassFixture interface with the TestingWebAppFactory class and inject it in a constructor, where we create an instance of the HttpClient. The IClassFixture interface is a marker interface, which indicates that tests in this class rely on a fixture to run. We can see that the fixture is our TestingWebAppFactory class.
xUnit creates one TestingWebAppFactory before the first test in the class runs, hands the same instance to every test, and disposes of it after the last one. So our app starts once for the whole class, even though xUnit creates a new instance of the test class for every test.
Now, let’s write our first integration test:
[Fact]
public async Task Index_WhenCalled_ReturnsApplicationForm()
{
var response = await _client.GetAsync("/Employees");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Mark", responseString);
Assert.Contains("Evelin", responseString);
}
We use the GetAsync method to call the action on the /Employees route, which is the Index action, and return a result in a response variable. With the EnsureSuccessStatusCode method, we verify that the IsSuccessStatusCode property is set to true.
If it is false, it means that the request is not successful, and the test will fail.
Finally, we serialize our HTTP content to a string using the ReadAsStringAsync method and verify that it contains our two employees.
Why Does Migrate() Fail With the In-Memory Database?
Right now, if we start the test runner, our test will fail due to the migration issue:
System.InvalidOperationException : Relational-specific methods can only be used when the context is using a relational database provider.
Why? Because WebApplicationFactory runs our real Program.cs, which calls app.MigrateDatabase() on startup, and our MigrationManager class can execute migrations only with the real SQL server and not with the in-memory one.
Migrations are a relational feature. They produce SQL, and the in-memory provider has no SQL to run. The stack trace points straight at it: RelationalDatabaseFacadeExtensions.Migrate().
So, to fix this issue, we are going to modify the try block in the EmployeesApp project’s Extensions/MigrationManager.cs, and skip migrations when the provider is the in-memory one:
try
{
if (appContext.Database.ProviderName != "Microsoft.EntityFrameworkCore.InMemory")
appContext.Database.Migrate();
}
EF Core also has a shorter check, appContext.Database.IsRelational(), which returns false for the in-memory provider. Either works here.
You’ll see the same error from many relational-only calls against the in-memory provider, such as ExecuteSqlRaw() or GetDbConnection(). The fix is the same: guard the call, or test against a relational database.
Now, we can run our test:
We can see that the test passed and that we successfully returned our employees from the in-memory database. If you want to ensure that we are really using the in-memory database and not the real one, you can always stop the SQLServer service in the Services window and run the test again.
Excellent!
Now, we can continue towards the integration testing of both Create actions.
Integration Testing of the Create (GET) Action in ASP.NET Core
Before we continue with testing, let’s open the Create.cshtml file, from the Views\Employees folder, and modify it by changing the h4 tag (just to have more than one word to test):
<h4>Please provide a new employee data</h4>
Great.
Now, we are ready to write our test code.
We want to verify when the Create (GET) action executes, it returns a create form:
[Fact]
public async Task Create_WhenCalled_ReturnsCreateForm()
{
var response = await _client.GetAsync("/Employees/Create");
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Please provide a new employee data", responseString);
}
And it does.
Testing the Create (POST) Action
To continue, we will write some integration testing code for the POST action. For the first test method, we will verify that our action returns a view with an appropriate error message when the model sent from the Create page, is invalid. And yes, in a previous article, we had test methods for the invalid model, but without an HTTP request.
That said, let’s write the test code:
[Fact]
public async Task Create_SentWrongModel_ReturnsViewWithErrorMessages()
{
var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Employees/Create");
var formModel = new Dictionary<string, string>
{
{ "Name", "New Employee" },
{ "Age", "25" }
};
postRequest.Content = new FormUrlEncodedContent(formModel);
var response = await _client.SendAsync(postRequest);
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("Account number is required", responseString);
}
We create a post request and the formModel object as a dictionary, which consists of the elements that we have on the Create page. Of course, we didn’t provide all the elements, the AccountNumber is missing because we want to send invalid data.
After that, we store the formModel as a content in our request, send that request with the SendAsync method and ensure that the response is successful.
Finally, we serialize our response and verify assertions.
If we take a look at the Employee model class, we are going to see that if the AccountNumber is not provided, the error message should appear on the form:
[Required(ErrorMessage = "Account number is required")]
public string? AccountNumber { get; set; }
That is exactly what we verify in our test method.
Now, we can run the Test Explorer:
Well, this test fails. But there is nothing wrong with the code. The test code is good. For some reason, we are getting the 400 Bad Request message.
Why is that?
The ValidateAntiForgeryToken Attribute Causing Integration Test to Fail
If we open our controller and look at the Create (POST) action, we can see the ValidateAntiForgeryToken attribute. Our action expects the anti-forgery token to be provided, but we are not doing that, so the test fails. For now (just as a temporary solution), we are going to comment out that attribute and run the test again.
Now, the test passes. As we said, this is just a temporary solution. There are a couple of steps required to configure the Anti-Forgery token in our testing code, and in the next article, we are going to show you how to do that step by step. For now, let’s just continue with another test while the ValidateAntiForgeryToken is commented out.
Testing Successful POST Request
Let’s write the final test in this article, where we verify that the Create action returns the Index view if the POST request is successful:
[Fact]
public async Task Create_WhenPOSTExecuted_ReturnsToIndexViewWithCreatedEmployee()
{
var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Employees/Create");
var formModel = new Dictionary<string, string>
{
{ "Name", "New Employee" },
{ "Age", "25" },
{ "AccountNumber", "214-5874986532-21" }
};
postRequest.Content = new FormUrlEncodedContent(formModel);
var response = await _client.SendAsync(postRequest);
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.Contains("New Employee", responseString);
Assert.Contains("214-5874986532-21", responseString);
}
So, this code is not too much different from the previous one, except we send a valid formModel object with the request and the assertion part. Basically, once the POST request is finished successfully, the Create method should redirect us to the Index method. There, we can find all the employees, including the created one. The factory’s client follows that redirect for us, so response already holds the Index page. You can always debug your test code and inspect the responseString variable to visually confirm that the response is the Index page with a new employee.
Once we run the Test Explorer, the test will pass.
Does WebApplicationFactory Work With Minimal APIs?
Yes, and the factory itself doesn’t change. It runs whatever Program.cs builds, so an app with MapGet() and MapPost() endpoints starts the same way our MVC app does.
IClassFixture shares it across a test class just like here, and on .NET 10 there’s nothing to add to Program.cs either.
What changes is the response: a minimal API usually returns JSON. So instead of searching the body for a string, we deserialize it with ReadFromJsonAsync<T>(), or call GetFromJsonAsync<T>() on the client, and assert on the typed result.
For request bodies, PostAsJsonAsync() serializes an object for us.
Reading the JSON the way a client does also catches a broken contract. Rename a property on the server, and your test’s copy of that property comes back with its default value, such as null, so the assertion on it fails.
A unit test that calls the handler directly never sees the JSON, so it can’t catch that.
Microsoft’s guide to unit and integration tests in minimal API apps covers the other half: unit tests for handlers that return TypedResults, plus a sample with both kinds of test.
Conclusion
In this article, we have learned about integration testing in ASP.NET Core with WebApplicationFactory. We have created an in-memory database to use during tests instead of the real database server, and fixed the two EF Core errors that stood in the way. Additionally, we have learned how to test our Index action and how to write integration tests for the Create actions. This testing methodology could also be applied to the other actions in our controller, and to minimal API endpoints.
Finally, we have seen the problem with the anti-forgery token. In the next article, we will learn how to solve that problem by introducing several new functionalities to our code.
Tested with .NET 10.0.10, EF Core 10.0.11 and xUnit 2.9.3.




Hi! You can create derived class in integration test project, from Program class in the main project and then you don’t need to add this: public partial class Program { } to your main project code.
Example:
public class ProgramTest : Program { } TestingWebAppFactory<ProgramTest> factory = new TestingWebAppFactory<ProgramTest>()Thanks David. Makes perfect sense.
Wonderful article! Everything works folowing the article and tests work as they are intended.
Awesome. Thank you for that info.
Hi,
I tried to replicate the test project and build it as per the code and instructions above
However, ConfigureWebHost method in the test project fails at two places in the code snippet reproduced
from your code below:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<EmployeeContext>));
if (descriptor != null)
services.Remove(descriptor);
services.AddDbContext<EmployeeContext>(options =>
{
options.UseInMemoryDatabase(“InMemoryEmployeeTest”);
});
var sp = services.BuildServiceProvider();
using (var scope = sp.CreateScope())
using (var appContext = scope.ServiceProvider.GetRequiredService<EmployeeContext>())
{
try
{
appContext.Database.EnsureCreated();
}
catch (Exception ex)
{
//Log errors or do anything you think it’s needed
throw;
}
}
});
}
The points of failure are the methods in bold (AddDbContext and BuildServiceProvider) with the error:
‘IServiceCollection’ does not contain a definition for ‘AddDbContext’ and no accessible extension method ‘AddDbContext’ accepting a first argument of type ‘IServiceCollection’ could be found (are you missing a using directive or an assembly reference?)
With a similar worded message for the BuildServiceProvider method.
Am I missing anything?
I have already wasted quite a few hours trying to debug/correct the error but in vain.
Please note that all I have chnaged is to use (LocalDb)\\MSSQLLocalDB as DataSource in the main project to create the database (with update-database command as Server = . in the connection string didn’t work for me but that shouldn’t affect this method, I guess
An early resolution to this issue will give your blog more credibility.
Hello Kaushik Roy Chowdhury. As this is a part of the entire Testing series, maybe you missed something from the previous articles. The error you provided seems to be the result of you missing installing EF Core in your main project thus you don’t have the required AddDbContext method. This method is part of the public static class EntityFrameworkServiceCollectionExtensions class, and you will have it only once you install the EF Core package.
I’ve just tested my code and everything works perfectly, so always use our source code to compare with yours, check all the libraries and the code to see if is there any difference compared to yours.
Please disregard my earlier detailed comment speaking about the errors.I have figured out where I was going wrong.
Cheers,
Kaushik
Very helpful, Thanks!
You are most welcome. I am glad it helps.
Nice article, congrats! I’m facing a problem in net 6, where I only have Program (not startup) and I’m loading this in memory server with WebApplicationFactory<Program> and the httpclient returned by CreateClient() works fine but I need to access to my endpoints from a third party, like a Browser or other Client. Do you know if there is an approach in this scenario without Startup? tks so much in adv!
I am not sure that I understand, but if you want to test how your client, you can use Selenium as described later in this series.
I’m implementing contact test with partner and I need to start my app but with the factory it is only accessible by its own httpclient. This pactnet try to call my endpoints using its own client, it acts like an external client. But if this factory approach only loads an in memory host I thing I have to do it in another way. Tks for you reply!
He needs to write tests for his client app.
I’m so sorry, I had problems with my dictionary. I meant I’m implementing contract tests using a fwk called pactnet. This pactnet fwk uses an internal client to call my host, that’s why I imagine it like an external client, like a browser. But web application factory returns a httpclient with CreateClient() an it looks that it is the only way to talk with the in memory host.
Hi Marinko,
>> “… and install a single NuGet package required for the testing purposes” (AspNetCore.Mvc.Testing, Microsoft.EntityFrameworkCore.InMemory)
That looks an awful lot like two packages. 🙂
Well a single one and then one more 🙂 Yeah, I will fix that.
Hello Marinko, is there a way to add a reference to the function under test in a way that IDE or code is aware which one is testing. This way I know which are tested and which are yet to be tested.
Event though this is not a popular answer, I must say that I am not sure. If you find the an answer, please share it with us. It would help a lot of us for sure.
OK, thank you for the reply Marinko. I cannot find any sources, at the moment at least.
Very helpful.
Thanks Kevin. I’m glad it proved useful to you.
Thank you very much!
how to test OData url using this process