We have covered a lot of different topics in the previous articles. We have learned how to use xUnit to write Unit tests for our Validation class and how to test our Controller class with its actions by using the Moq library to isolate dependencies.
In this article, we are going to learn about Integration Testing in ASP.NET Core MVC. Additionally, we are going to prepare an in-memory database so we don’t have to use the real SQL server during integration tests.
You can download the source code on our GitHub repository.
For the complete navigation of this series, you can visit ASP.NET Core MVC Testing.
These are the topics we are going to cover:
- Preparing a New Project for Integration Testing
- Creating In-Memory Factory Configuration
- Integration Testing of the Index Action
- Testing the Create (GET) Action
- Additional Tests for the Create (POST) Action
- Conclusion
Let’s move on.
Preparing a new Project for Integration Testing
First, we are going to create a new xUnit
project named EmployeesApp.IntegrationTests
for integration testing purposes.
After the project creation, we are going to rename the UnitTest1.cs
class to EmployeesControllerIntegrationTests
:
Additionally, we are going to add a reference to the main project and install a single NuGet package required for the testing purposes:
- AspNetCore.Mvc.Testing – this package provides the TestServer and an important class
WebApplicationFactory
to help us bootstrap our app in-memory - Microsoft.EntityFrameworkCore.InMemory – In-memory database provider
Now we can continue on.
Creating In-Memory Factory Configuration
Let’s create a new class TestingWebAppFactory
and modify it accordingly:
public class TestingWebAppFactory<T> : WebApplicationFactory<Startup> { 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); } var serviceProvider = new ServiceCollection() .AddEntityFrameworkInMemoryDatabase() .BuildServiceProvider(); services.AddDbContext<EmployeeContext>(options => { options.UseInMemoryDatabase("InMemoryEmployeeTest"); options.UseInternalServiceProvider(serviceProvider); }); 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.
Our class implements the WebApplicationFactory<Startup>
class and overrides the ConfigureWebHost
method. In that method, we remove the EmployeeContext registration from the Startup.cs class. Then, we are adding Entity Framework in-memory database support to the DI container via the ServiceCollection
class.
After that, we add the database context to the service container and instruct it to use the in-memory database instead of the real database.
Finally, we ensure that we seed the data from the EmployeeContext
class (The same data you inserted into a real SQL Server database at the beginning of this series).
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 implement a previously created TestingWebAppFactory
class:
public class EmployeesControllerIntegrationTests : IClassFixture<TestingWebAppFactory<Startup>> { private readonly HttpClient _client; public EmployeesControllerIntegrationTests(TestingWebAppFactory<Startup> factory) { _client = factory.CreateClient(); } }
So, we implement the
TestingWebAppFactory
class with the IClassFixture
interface and inject it in a constructor, where we create an instance of the HttpClient
. The IClassFixture
interface is a decorator which indicates that tests in this class rely on a fixture to run. We can see that the fixture is our TestingWebAppFactory
class.
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 has the value true:
If the value is false, it would mean that the request is not successful, thus the test would fail.
Finally, we serialize our HTTP content to a string with the ReadAsStringAsync
method and verify that it contains our two employees:
We can see that the test passes and that we successfully return our employees from the in-memory database. If you want to make sure 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.
Testing the Create (GET) Action
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.
Additional Tests for the Create (POST) Action
So, let’s write some integration testing code for the POST action. For the first test method, we are going to 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.
Having 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 make assertion verification.
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, just for some reason we are getting the 400 Bad Request message.
Why is that?
Explanation
Well, if we open our controller and take a look at the Create (POST) action, we can see the ValidateAntiForgeryToken attribute. So, our action expects the anti-forgery token to be provided but we are not doing that, thus the test fails. For now (just as temporary solution) we are going to comment out that attribute and run the test again:
The result:
Now, the test passes. As we said this is just a temporary solution. There are a couple of steps required to configure 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. You can always debug your test code and inspect the responseString
variable to visually confirm that response is the Index
page with a new employee.
Finally, let’s run the Test Explorer:
Excellent! It passes.
Conclusion
In this article, we have learned how to write integration tests in the ASP.NET Core MVC application. We have created an In-Memory database to use it during tests instead of the real database server. Additionally, we have learned how to test our Index action and how to write integration tests for the Create actions as well. This testing methodology could be applied to other actions as well (PUT, Delete…).
Finally, we have seen the problem with the anti-forgery token and in the next article, we are going to learn how to solve that problem by introducing several new functionalities to our code.
how to test OData url using this process
Thank you very much!
Very helpful.
Thanks Kevin. I’m glad it proved useful to you.
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.