Integration Testing in ASP.NET Core with WebApplicationFactory
Integration testing is a crucial step in ensuring that your application behaves correctly when different components interact with one another. In ASP.NET Core, the WebApplicationFactory<TEntryPoint>
class makes it easier to test your web applications by allowing you to spin up an instance of your app in-memory and run integration tests against it.
As a new author contributing to the VSLive! Blog, in my inaugural post I'll walk you through how to use WebApplicationFactory
for integration testing, using a simple ASP.NET Core web application. We'll write tests to ensure that the Home page is functioning as expected. Let's dive in!
Structure of the Sample Project
This post includes a code sample with "before" and "after" versions of the code.
In the solution shown above we have a total of four projects. We have two main projects:
- API Project: A .NET Core class library containing business logic.
- Web Project: An ASP.NET MVC application with the front-end logic.
Additionally, we have two test projects:
- Unit Tests: For testing the API logic in isolation.
- Integration Tests: For testing interactions between components, such as controllers and views, within the actual application.
We'll focus on the Integration Tests project, using WebApplicationFactory
to create tests that check if the web application behaves correctly in real-world scenarios.
What Is WebApplicationFactory?
WebApplicationFactory<TEntryPoint>
is a handy class in ASP.NET Core that allows us to spin up our application in-memory for testing purposes. This eliminates the need for dealing with network ports or external dependencies during testing. By creating an in-memory version of the application, we can run HTTP requests against it, similar to how we would interact with the deployed app.
Creating an Integration Test
Let's create a test to verify that the Home page of our web application displays the text "Hello World." To get started, open your Integration Test project, right-click, and add a new class. We'll name it HomeControllerFixture
.
Writing the Test -- Here's the basic structure of our test:
public async void IndexContainsHelloWorld()
{
// arrange
var factory = new WebApplicationFactory<JustAnEmptyClass>();
var client = factory.CreateClient();
// act
var response = await client.GetAsync("/");
// assert
Assert.NotNull(response);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Hello, world!", content);
Assert.True(response.IsSuccessStatusCode);
}
Breakdown of the Test:
-
Arrange:
- We create an instance of
WebApplicationFactory<JustAnEmptyClass>
and use it to spin up an in-memory version of our app.
- Using
CreateClient()
, we get an instance of HttpClient
that knows how to communicate with the in-memory application.
-
Act:
- The
client.GetAsync("/")
method makes an HTTP GET
request to the root of the application, similar to navigating to the home page in a browser.
-
Assert:
- We use
EnsureSuccessStatusCode()
to check if the request was successful (i.e., it returned a 200 status code).
- We then read the response content and check that it contains the string "Hello World" using
Assert.Contains()
.
Handling the Missing Startup
Class
You might notice that we reference a class called JustAnEmptyClass
instead of the Startup
class. Here's why: In older ASP.NET Core projects, the Startup.cs file was used to configure the application. However, in newer project templates, the Program.cs file has taken over this role, combining the functionality of both Program
and Startup
. This is all part of the change to add "top-level statements" to C#. On the one hand, it simplifies the code. On the other hand, it sometimes gets in the way, and this is one of those cases where it gets in the way.
If your project actually has a Startup.cs
class, then you can use that (WebApplicationFactory
) instead of creating this empty, marker class. Or if you'd like to have your project actually use that style of project you can easily do that by adding the --use-program-main
option to your dotnet new
command when you create your project.
Unfortunately, WebApplicationFactory
doesn't work directly with the Program
class. The workaround is simple: create an empty class and use it as a placeholder for WebApplicationFactory
. This doesn't interfere with the functionality of your application but allows you to run your tests.
// JustAnEmptyClass.cs
public class JustAnEmptyClass
{
// This class is intentionally left empty.
}
Now, when creating the factory, we reference JustAnEmptyClass
instead of Startup
or Program
.
Running the Tests: It Fails
After writing the test, run it from your Test Explorer window. If you encounter an error that the test is missing, check that you've added the [Fact]
attribute to the test method -- this tells the test framework that it's a test that should be run.
Upon running the test, it should fail initially because the home page doesn't yet contain the string "Hello World."
Fixing the Failure
In our sample application, fixing this problem is easy enough. There's nothing on the home page that says "hello world" and therefore we just need to add that. In real-life development situations, you'll probably be testing for more complex things and you might need to write some real code.
To fix this, let's add "Hello World" to our Home page.
- In Solution Explorer, open up the web application and go to the Views folder and then the Home folder.
- Open Index.cshtml.
- Add a paragraph containing "Hello World."
- Save the file and rerun the test.
When you've made the changes to Index.cshtml, it should look something like this:
This time, when you run the tests, the test should pass, confirming that our integration test successfully verifies the content of the home page.
Conclusion
Using WebApplicationFactory
, we can easily write integration tests that spin up an in-memory version of our ASP.NET Core application. This allows us to test the interaction between controllers, views, and other components without needing to deploy the app to a server.
In this demo, we wrote a test to check that the Home page contains "Hello World," but you can extend this approach to test more complex scenarios like API endpoints, form submissions and more.
Posted by Benjamin Day on 09/23/2024