Integration testing

Compared to unit tests, integration tests exercise the whole application stack (routing, controllers, services, database). Instead of isolating one class or component, integration tests ensure that all of the components of your application are working together properly.

Integration tests are slower and more involved than unit tests, so it’s common for a project to have lots of unit tests but only a handful of integration tests.

In order to test the whole stack (including controller routing), integration tests typically make HTTP calls to your application just like a web browser would.

To write integration tests that make HTTP requests, you could manually start your application run tests that make requests to http://localhost:5000 (and hope the app is still running). ASP.NET Core provides a nicer way to host your application for testing, however: using the TestServer class. TestServer can host your application for the duration of the test, and then stop it automatically when the test is complete.

Create a test project

You could keep your unit tests and integration tests in the same project (feel free to do so), but for the sake of completeness, I’ll show you how to create a separate project for your integration tests.

If you’re currently in your project directory, cd up one level to the base AspNetCoreTodo directory. Use these commands to scaffold a new test project:

  1. mkdir AspNetCoreTodo.IntegrationTests
  2. cd AspNetCoreTodo.IntegrationTests
  3. dotnet new xunit

Your directory structure should now look like this:

  1. AspNetCoreTodo/
  2. AspNetCoreTodo/
  3. AspNetCoreTodo.csproj
  4. Controllers/
  5. (etc...)
  6. AspNetCoreTodo.UnitTests/
  7. AspNetCoreTodo.UnitTests.csproj
  8. AspNetCoreTodo.IntegrationTests/
  9. AspNetCoreTodo.IntegrationTests.csproj

Since the test project will use the classes defined in your main project, you’ll need to add a reference to the main project:

  1. dotnet add reference ../AspNetCoreTodo/AspNetCoreTodo.csproj

You’ll also need to add the Microsoft.AspNetCore.TestHost NuGet package:

  1. dotnet add package Microsoft.AspNetCore.TestHost

Delete the UnitTest1.cs file that’s created by dotnet new. You’re ready to write an integration test.

Write an integration test

There are a few things that need to be configured on the test server before each test. Instead of cluttering the test with this setup code, you can factor out this setup to a separate class. Create a new class called TestFixture:

AspNetCoreTodo.IntegrationTests/TestFixture.cs

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Net.Http;
  5. using Microsoft.AspNetCore.Hosting;
  6. using Microsoft.AspNetCore.TestHost;
  7. using Microsoft.Extensions.Configuration;
  8. namespace AspNetCoreTodo.IntegrationTests
  9. {
  10. public class TestFixture : IDisposable
  11. {
  12. private readonly TestServer _server;
  13. public TestFixture()
  14. {
  15. var builder = new WebHostBuilder()
  16. .UseStartup<AspNetCoreTodo.Startup>()
  17. .ConfigureAppConfiguration((context, configBuilder) =>
  18. {
  19. configBuilder.SetBasePath(Path.Combine(
  20. Directory.GetCurrentDirectory(), "..\\..\\..\\..\\AspNetCoreTodo"));
  21. configBuilder.AddJsonFile("appsettings.json");
  22. // Add fake configuration for Facebook middleware (to avoid startup errors)
  23. configBuilder.AddInMemoryCollection(new Dictionary<string, string>()
  24. {
  25. ["Facebook:AppId"] = "fake-app-id",
  26. ["Facebook:AppSecret"] = "fake-app-secret"
  27. });
  28. });
  29. _server = new TestServer(builder);
  30. Client = _server.CreateClient();
  31. Client.BaseAddress = new Uri("http://localhost:5000");
  32. }
  33. public HttpClient Client { get; }
  34. public void Dispose()
  35. {
  36. Client.Dispose();
  37. _server.Dispose();
  38. }
  39. }
  40. }

This class takes care of setting up a TestServer, and will help keep the tests themselves clean and tidy.

If you configured Facebook login in the Security and identity chapter., it’s necessary to add fake values for the Facebook app ID and secret (in the ConfigureAppConfiguration block above). This is because the test server doesn’t have access to the values in the Secrets Manager. Adding some fake values in this fixture class will prevent an error when the test server starts up.

Now you’re (really) ready to write an integration test. Create a new class called TodoRouteShould:

AspNetCoreTodo.IntegrationTests/TodoRouteShould.cs

  1. using System.Net;
  2. using System.Net.Http;
  3. using System.Threading.Tasks;
  4. using Xunit;
  5. namespace AspNetCoreTodo.IntegrationTests
  6. {
  7. public class TodoRouteShould : IClassFixture<TestFixture>
  8. {
  9. private readonly HttpClient _client;
  10. public TodoRouteShould(TestFixture fixture)
  11. {
  12. _client = fixture.Client;
  13. }
  14. [Fact]
  15. public async Task ChallengeAnonymousUser()
  16. {
  17. // Arrange
  18. var request = new HttpRequestMessage(HttpMethod.Get, "/todo");
  19. // Act: request the /todo route
  20. var response = await _client.SendAsync(request);
  21. // Assert: anonymous user is redirected to the login page
  22. Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
  23. Assert.Equal("http://localhost:5000/Account/Login?ReturnUrl=%2Ftodo",
  24. response.Headers.Location.ToString());
  25. }
  26. }
  27. }

This test makes an anonymous (not-logged-in) request to the /todo route and verifies that the browser is redirected to the login page.

This scenario is a good candidate for an integration test, because it involves multiple components of the application: the routing system, the controller, the fact that the controller is marked with [Authorize], and so on. It’s also a good test because it ensures you won’t ever accidentally remove the [Authorize] attribute and make the to-do view accessible to everyone.

Run the test in the terminal with dotnet test. If everything’s working right, you’ll see a success message:

  1. Starting test execution, please wait...
  2. [xUnit.net 00:00:00.7237031] Discovering: AspNetCoreTodo.IntegrationTests
  3. [xUnit.net 00:00:00.8118035] Discovered: AspNetCoreTodo.IntegrationTests
  4. [xUnit.net 00:00:00.8779059] Starting: AspNetCoreTodo.IntegrationTests
  5. [xUnit.net 00:00:01.5828576] Finished: AspNetCoreTodo.IntegrationTests
  6. Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
  7. Test Run Successful.
  8. Test execution time: 2.0588 Seconds

Wrap up

Testing is a broad topic, and there’s much more to learn. This chapter doesn’t touch on UI testing or testing frontend (JavaScript) code, which probably deserve entire books of their own. You should, however, have the skills and base knowledge you need to practice and learn more about writing tests for your own applications.

As always, the ASP.NET Core documentation (https://docs.asp.net) and StackOverflow are good resources for learning more and finding answers when you get stuck.