Integration Testing Your Web APIs

Integration Testing. Without a server. AND database? Yes, it can be done. In today's post, we demonstrate this awesome capability.

Written by Jonathan "JD" Danylko • Last Updated: • Develop •
Chalkboard with the word TEST on it.

With JavaScript frameworks like Angular and React, every Single-Page Application (SPA) requires API calls for the application to function properly.

You always have to test your APIs before deploying them with your application. Unit tests are great for unit level code, but how could you test your API in a dev-related environment with test data?

There's always mocking frameworks, but it just isn't the same.

You could always create a new environment, but it would require a test database.

So what do you do?

Build a Virtual Server

When I first found out about this, I was blown away.

With ASP.NET Core 2.1 and higher, you can create a virtual server to "integration test" your API...

...with test data!

I first found out about this when I watched the Visual Studio Toolbox on Channel 9 with Chris Woodruff explaining how to build web APIs with ASP.NET Core.

Then I decided to check it out further and found more details about Integration Tests in ASP.NET Core on doc.microsoft.com

The basis is built on a new class called WebApplicationFactory. This allows you to take your existing Startup in your API project and use it for integration tests.

It can even has the capabilities to use Entity Framework's InMemory Database to return test records.

Setting up the project

In your solution, you should have your API project and a unit test project available (if you don't have a unit test project, shame on you). ;-)

In your unit test project, create a new folder called IntegrationTests.

I also created an Infrastructure folder for the "integration server."

The TestHost.cs is the server piece of the integration tests and shows a number of interesting concepts.

UnitTestProject1\Infrastructure\TestHost.cs

using System;
using APIIntegrationDemo;
using APIIntegrationDemo.Context;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace UnitTestProject1.Infrastructure
{
    public class TestHost<TStartup> : WebApplicationFactory<Startup>
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                // Create a new service provider.
                var serviceProvider = new ServiceCollection()
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                // Add a database context (AppDbContext) using an in-memory database for testing.                 services.AddDbContext<DisneyContext>(options =>                 {                     options.UseInMemoryDatabase("IntegrationTests");                     options.UseInternalServiceProvider(serviceProvider);                 });
                // Build the service provider.                 var sp = services.BuildServiceProvider();
                // Create a scope to obtain a reference to the database contexts                 using (var scope = sp.CreateScope())                 {                     var scopedServices = scope.ServiceProvider;                     var context = scopedServices.GetRequiredService<DisneyContext>();
                    var logger = scopedServices.GetRequiredService<ILogger<TestHost<TStartup>>>();
                    // Ensure the database is created.                     context.Database.EnsureCreated();
                    try                     {                         // Seed your [fake] database with some specific test data.                         context.SeedFakeDatabase();                     }                     catch (Exception ex)                     {                         logger.LogError(ex, "An error occurred seeding the " +                                             "database with test messages. Error: {ex.Message}");                     }                 }             });         }     } }

First, we prepare our InMemory database by adding an existing DbContext. This is the context from your API project.

Once we have a DbContext, we can build our InMemory database and populate it with test data.

After filling the database quickly with test data through Resource strings, we now have our integration server ready to go.

That's it? You're kidding me?

Yes, that's pretty much it.

This "hosting harness" is what blew my mind when I found out how easy this was in ASP.NET Core 2.1 or higher.

What does our integration tests look like?

Our integration test only requires a TestHost and the actual Web API call.

using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using APIIntegrationDemo;
using APIIntegrationDemo.Entities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using UnitTestProject1.Infrastructure;

namespace
 UnitTestProject1.IntegrationTests {     [TestClass]     public class AttractionApiTest     {         private TestHost<Startup> _server;         private HttpClient _client;
        [TestInitialize]         public void AttractionApiTestSetup()         {             _server = new TestHost<Startup>();             _client = _server.CreateClient();         }
        [TestCategory("Integration")]         [TestMethod]         public async Task GetTwoAttractionsTest()         {             // Arrange             var httpResponse = await _client.GetAsync("/api/Attraction");
            // MUST be successful.             httpResponse.EnsureSuccessStatusCode();
            // Act             var stringResponse = await httpResponse.Content.ReadAsStringAsync();             var attractions = JsonConvert.DeserializeObject<List<Attraction>>(stringResponse);
            // Assert             Assert.IsNotNull(attractions);             Assert.IsTrue(attractions.Count == 2);         }     } }

When we receive the response from the controller, we deserialize the string into our objects so we can now use them in our application.

You can even test any verbs in your APIs: GET, POST, PUT, DELETE.

This makes your integration tests for your APIs so much easier.

Code can be found here.

Conclusion

I love how this technique pushes the evolution of web testing even further to make your applications more stable and reliable.

Since your APIs are by nature decoupled, there should be no reason why your APIs would fail when you have these integration tests.

This, itself, is probably the best reason to move to ASP.NET Core since the web has turned into a giant Web API.

How do you perform integration testing? Do you have a separate environment? Do you HAVE an environment? Post your comments below and let's discuss.

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus