Integration testing in ASP.NET MVC 6

By Dawid on (tags: asp.net 5, mvc 6, testing, categories: code)

In ASP.NET 5, Microsoft has produced a Nuget package called Microsoft.AspNet.TestHost which you can easily use to run your ASP.NET 5 in memory. We can use that feature of self hosted application to create integration tests.

Creating integration test project

After creating a new ASP.NET 5 solution open project.json file and dependency for XUnit.DNX integration packages and the Microsoft.AspNet.TestHost package. Here is an example file:

   1: "dependencies": {
   2:     "Microsoft.AspNet.Mvc": "6.0.0-beta7",
   3:     "Microsoft.AspNet.Server.IIS": "1.0.0-beta7",
   4:     "Microsoft.AspNet.Server.WebListener": "1.0.0-beta7",
   5:     "Microsoft.AspNet.StaticFiles": "1.0.0-beta7",
   6:     "Microsoft.AspNet.TestHost": "1.0.0-beta7",
   7:     "xunit": "2.1.0-beta2-build2981",
   8:     "xunit.runner.dnx": "2.1.0-beta2-build79"
   9:   }

After this we are ready to write simple controller which we use in our tests. Take a look at this:

   1: [Route("api/[controller]")]
   2: public class TestController : Controller
   3: {
   4:     [HttpGet]
   5:     public IEnumerable<string> Get()
   6:     {
   7:         return new string[] { "value100", "value200" };
   8:     }
   9:  
  10:     [HttpGet("{id}")]
  11:     public IActionResult Get(int id)
  12:     {
  13:         if (id <= 0)
  14:         {
  15:             return new BadRequestObjectResult("ID must be larger than 0");
  16:         }
  17:  
  18:         return new ObjectResult("test value");
  19:     }
  20:  
  21:     [HttpPost]
  22:     public void Post([FromBody]string value)
  23:     {
  24:     }
  25: }

We will use Microsoft.AspNet.TestHost.TestServer class which exposes a lot of factory methods depending on how much set up you want to do. The server will be created as http://localhost unless you set the BaseAddress property to something else. In fact the URL is not so important as long as server and client agree on the save value. As I mentioned before, the factory methods on the TestServer make it easy to integrate your Startup class into the tests. Here is an example of class which will set up our tests:

   1: public class SimpleIntegrationTests
   2: {
   3:     private readonly Action<IApplicationBuilder> _app;
   4:     private readonly Action<IServiceCollection> _services;
   5:  
   6:     public Tests()
   7:     {
   8:         var environment = CallContextServiceLocator.Locator.ServiceProvider.GetRequiredService<IApplicationEnvironment>();
   9:  
  10:         var startup = new Startup(new HostingEnvironment(environment));
  11:         _app = startup.Configure;
  12:         _services = startup.ConfigureServices;
  13:     }
  14: }

And here are out simple integration tests – we will test our example controller created above:

   1: [Fact]
   2:     public async Task GetAllValues_OK()
   3:     {
   4:         // Arrange
   5:         var server = TestServer.Create(_app, _services);
   6:         var client = server.CreateClient();
   7:  
   8:         // Act
   9:         var response = await client.GetAsync("http://localhost/api/values");
  10:         var deserialized = await response.Content.ReadAsStringAsync();
  11:  
  12:         // Assert
  13:         Assert.Equal(HttpStatusCode.OK, response.StatusCode);
  14:         Assert.Equal(@"[""value100"",""value200""]", deserialized);
  15:     }
  16:  
  17:     [Fact]
  18:     public async Task GetSingleValue_IDNotInt_BadRequest()
  19:     {
  20:         // Arrange
  21:         var server = TestServer.Create(_app, _services);
  22:         var client = server.CreateClient();
  23:  
  24:         // Act
  25:         var response = await client.GetAsync("http://localhost/api/values/0");
  26:  
  27:         // Assert
  28:         Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
  29:     }
And that’s all. As you can see this approach is very simple and what’s worth mentioning - all end-to-end tests in the MVC 6 repository use the same approach!