Integration testing ASP.NET 5 and ASP.NET MVC 6 applications

Β· 947 words Β· 5 minutes to read

The other day I ran into a post by Alex Zeitler, who blogged about integration testing of ASP.NET MVC 6 controllers. Alex has done some great work for the Web API community in the past and I always enjoy his posts.

In this case, Alex suggested using self hosting for that, so spinning up a server and hitting it over HTTP and then shutting down, as part of each test case. Some people have done that in the past with Web API too, but is not an approach I agree with when doing integration testing. If you follow this blog you might have seen my post about testing OWIN apps and Web API apps in memory already.

My main issue with the self-host approach, is that you and up testing the underlying operating system networking stack, the so-called “wire” which is not necessarily something you want to test - given that it will be different anyway in production (especially if you intend to run on IIS). On the other hand, you want to be able to run end-to-end tests quickly anywhere - developer’s machine, integration server or any other place that it might be necessary, and doing it entirely in memory is a great approach.

Microsoft.AspNet.TestHost πŸ”—

In the past, to memory host a Web API application you’d use an HttpServer type, which was a part of the framework. To memory host an OWIN pipeline you could use Microsoft.Owin.Testing package or Damian Hickey’s OwinHttpMessageHandler.

In ASP.NET 5 (DNX world), Microsoft has produced a Nuget package called Microsoft.AspNet.TestHost which you can easily utilize to run your ASP.NET 5 in memory instead of over the wire.

In fact, the aforementioned OwinHttpMessageHandler is also DNX compatible now, but it operates on a lower sets of abstractions, such as AppFunc and MidFunc. However, if you are doing pure OWIN development, this is definitely something you’d want to check out.

On the other hand Microsoft.AspNet.TestHost allows you to plug in the Action and Action which will likely be already existing as part of your ASP.NET 5 Startup class.

Putting it together πŸ”—

To test an ASP.NET 5 application you will need the new XUnit.DNX integration packages and the Microsoft.AspNet.TestHost package. Here are my project.json dependencies (xunit Visual Studio integration for DNX projects is entirely contained in the *xunit.runner.dnx* package):

"dependencies": {  
"Microsoft.AspNet.Mvc": "6.0.0-beta4",  
"Microsoft.AspNet.Server.IIS": "1.0.0-beta4",  
"Microsoft.AspNet.Server.WebListener": "1.0.0-beta4",  
"Microsoft.AspNet.StaticFiles": "1.0.0-beta4",  
"Microsoft.AspNet.TestHost": "1.0.0-beta4",  
"xunit": "2.1.0-beta2-build2981",  
"xunit.runner.dnx": "2.1.0-beta2-build79"  
},  

Next, let’s imagine a test controller and a default Startup class - of course in your application those will be much more complicated, but for illustration purposes the defaults that come with the MVC 6 project templates are enough. I also added a simple validation to one of the actions, just so that we have a code path with non-successful HTTP response code.

[Route("api/[controller]")]  
public class ValuesController : Controller  
{  
[HttpGet]  
public IEnumerable<string> Get()  
{  
return new string[] { "value1", "value2" };  
}

[HttpGet("{id}")]  
public IActionResult Get(int id)  
{  
if (id <= 0) { return new BadRequestObjectResult("ID must be larger than 0"); } 
return new ObjectResult("value"); } 

[HttpPost] 
public void Post([FromBody]string value) { } 
} 

public class Startup { 
  public Startup(IHostingEnvironment env) { } 
  
  public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } 
  public void Configure(IApplicationBuilder app) 
  { 
    // Configure the HTTP request pipeline. app.UseStaticFiles(); // Add MVC to the request pipeline. app.UseMvc(); 
  } 
}

Microsoft.AspNet.TestHost.TestServer is the class that you will use, and it exposes a bunch of factory methods depending on how much set up you want to do:

public static TestServer Create(Action<IApplicationBuilder> configureApp);  
public static TestServer Create(IServiceProvider serviceProvider, Action<IApplicationBuilder> configureApp);  
public static TestServer Create(Action<IApplicationBuilder> configureApp, Action<IServiceCollection> configureServices);  
public static TestServer Create(IServiceProvider serviceProvider, Action<IApplicationBuilder> configureApp, ConfigureServicesDelegate configureServices);  
public static TestServer Create(IServiceProvider serviceProvider, Action<IApplicationBuilder> configureApp, Action<IServiceCollection> configureServices);  

The server is being created as http://localhost unless you set the BaseAddress property to something else (i.e. if you want https binding). The actual URL is not important and can be anything - as long as the server and client agree on the same value.

As mentioned before, the factory methods on the TestServer make it easy to integrate your Startup class into the tests.

Below is an example of the setup of tests:

public class IntegrationTests  
{  
private readonly Action<IApplicationBuilder> _app;  
private readonly Action<IServiceCollection> _services;

public Tests()  
{  
var environment = CallContextServiceLocator.Locator.ServiceProvider.GetRequiredService<IApplicationEnvironment>();

var startup = new Startup(new HostingEnvironment(environment));  
_app = startup.Configure;  
_services = startup.ConfigureServices;  
}

//write your individual tests here  
}  

All that’s left at this point is to simply create the instance of a TestServer in each individual test, pass in the delegates saved in the fields, and create an HttpClient off the server (TestServer exposes a method CreateClient). At that point, you use the instantiated HttpClient just like you are used to - except all interaction with the “server” happens in memory.

Below are a few example tests against our test controller shown earlier:

[Fact]  
public async Task GetAllValues_OK()  
{  
// Arrange  
var server = TestServer.Create(\_app, \_services);  
var client = server.CreateClient();

// Act  
var response = await client.GetAsync("http://localhost/api/values");  
var deserialized = await response.Content.ReadAsStringAsync();

// Assert  
Assert.Equal(HttpStatusCode.OK, response.StatusCode);  
Assert.Equal(@"[""value1&#8243;",""value2&#8243;"]", deserialized);  
}

[Fact]  
public async Task GetSingleValue\_IDNotInt\_BadRequest()  
{  
// Arrange  
var server = TestServer.Create(\_app, \_services);  
var client = server.CreateClient();

// Act  
var response = await client.GetAsync("http://localhost/api/values/0");

// Assert  
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);  
}

[Fact]  
public async Task PostValue_NoContent()  
{  
// Arrange  
var server = TestServer.Create(\_app, \_services);  
var client = server.CreateClient();

// Act  
var value = new StringContent("foo");  
var response = await client.PostAsync("http://localhost/api/values", value);

// Assert  
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);  
}  

And the result:

Integration tests

In each of the above cases we are able to run robust end-to-end integration in a very fast way, without worrying about the network stack.

What’s worth mentioning is that all end-to-end tests in the MVC 6 repository use the same approach.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP