Alexander Zeitler

Integration-testing ASP.NET 5 / MVC 6 Controllers on DNX Beta 4

Published on Saturday, May 16, 2015

In ASP.NET 5 bootstrapping a self host test server has changed compared to ASP.NET 4 / Web API.

This post will show you how you can run integration tests using ASP.NET 5 / MVC 6 API Controllers on Beta 4 of DNX.

First, the Controller implementation:

CustomersController.cs:

[Route("api/[controller]")]
public class CustomersController
{
    [HttpGet]
    public IActionResult GetAll()
    {
        return new ObjectResult(
            new List<Customer>()
            {
                new Customer
                {
                    CompanyName = "PDMLab",
                    AddressLine = "Ludwig-Erhard-Allee 10",
                    ZipCode = "76131",
                    City = "Karlsruhe",
                    Website = "https://pdmlab.com"
                }
            });
    }
}

Our tests will use Xunit, so we'll add Xunit to our project.json file:

...
 "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",
    "xunit": "2.1.0-*",
    "xunit.runner.dnx": "2.1.0-beta2-build79",
    "xunit.runner.visualstudio": "2.1.0-beta1-build1051"
  },
...

Furthermore, our HttpClient we're using resides in Microsoft.AspNet.WebApi.Client which we'll add also to our project.json file:

...
 "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.WebApi.Client": "5.2.3",
    "xunit": "2.1.0-*",
    "xunit.runner.dnx": "2.1.0-beta2-build79",
    "xunit.runner.visualstudio": "2.1.0-beta1-build1051"
  },
...

Finally, we need Kestrel to run our tests on *nix based systems as well as Microsoft.AspNet.Hosting which contains the classes required for bootstrapping a self host server - :

...
  "dependencies": {
    "Kestrel": "1.0.0-beta4",
    "Microsoft.AspNet.Hosting": "1.0.0-beta4",
    "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.WebApi.Client": "5.2.3",
    "xunit": "2.1.0-*",
    "xunit.runner.dnx": "2.1.0-beta2-build79",
    "xunit.runner.visualstudio": "2.1.0-beta1-build1051"
  },
...

The final part required is the test itself:

public class CustomerControllerIntegrationTest
    {
        [Fact]
        public void ShouldReturnCustomer()
        {
            var config = new Configuration();
            config.AddCommandLine(new[] { "--server.urls", "http://localhost:5001" });
            
            var serverFactoryLocation = string.Empty;
            if(!IsMono()) {
                serverFactoryLocation = "Microsoft.AspNet.Server.WebListener";
            } else {
                serverFactoryLocation = "Kestrel";
            }
            var context = new HostingContext()
            {
                Configuration = config,
                ServerFactoryLocation = serverFactoryLocation,
                ApplicationName = "AspNet5IntegrationTesting",
                StartupMethods = new StartupMethods(builder => builder.UseMvc(), services =>
                {
                    services.AddMvc();
                    return services.BuildServiceProvider();
                })
            };


            using (new HostingEngine().Start(context))
            {
                var client = new HttpClient();
                var customers = client.GetAsync("http://localhost:5001/api/customers").Result
                    .Content.ReadAsAsync<List<Customer>>().Result;

                Assert.Equal(customers[0].CompanyName,"PDMLab");
            }
        }
        
        public static bool IsMono()
        {
            return Type.GetType("Mono.Runtime") != null;
        }
 
    }

The implementation is pretty straight forward:

First we're adding the host and port our test server should listen on to our application configuration.

Then we're checking if we're running on mono and depdending on the result, we're bootstrapping Kestrel oder WebListener.

After that we're bootstrapping a HostingContext instance which configures our StartupMethods (which contains the stuff that resides in your Startup.cs normally), Configuration and the host to use.

Using this context we're firing up a new HostingEngine instance which gets disposedd after the assertions have finished.

That's it... almost, because there's one caveat:

The tests currently fail on Kestrel / *nix with this error:

xUnit.net DNX test runner (64-bit DNX 4.5.1)
Copyright (C) 2015 Outercurve Foundation.

Discovering: AspNet5IntegrationTesting
Discovered:  AspNet5IntegrationTesting
Starting:    AspNet5IntegrationTesting
   AspNet5IntegrationTesting.Tests.CustomerControllerIntegrationTest.ShouldReturnCustomer [FAIL]
      System.ArgumentException : GCHandle value belongs to a different domain
      Stack Trace:
           at System.Runtime.InteropServices.GCHandle.op_Explicit (IntPtr value) [0x00000] in <filename unknown>:0 
           at System.Runtime.InteropServices.GCHandle.FromIntPtr (IntPtr value) [0x00000] in <filename unknown>:0 
           at Microsoft.AspNet.Server.Kestrel.Networking.UvMemory.DestroyMemory (IntPtr memory) [0x00000] in <filename unknown>:0 
           at Microsoft.AspNet.Server.Kestrel.Networking.UvLoopHandle.ReleaseHandle () [0x00000] in <filename unknown>:0 
           at System.Runtime.InteropServices.SafeHandle.RunRelease () [0x00000] in <filename unknown>:0 
           at System.Runtime.InteropServices.SafeHandle.Dispose (Boolean disposing) [0x00000] in <filename unknown>:0 
           at System.Runtime.InteropServices.SafeHandle.Dispose () [0x00000] in <filename unknown>:0 
           at Microsoft.AspNet.Server.Kestrel.KestrelThread.ThreadStart (System.Object parameter) [0x00000] in <filename unknown>:0 
         --- End of stack trace from previous location where exception was thrown ---
           at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x00000] in <filename unknown>:0 
           at Microsoft.AspNet.Server.Kestrel.KestrelThread.Stop (TimeSpan timeout) [0x00000] in <filename unknown>:0 
           at Microsoft.AspNet.Server.Kestrel.KestrelEngine.Dispose () [0x00000] in <filename unknown>:0 
           at Kestrel.ServerFactory+<>c__DisplayClass3_0.<Start>b__1 () [0x00000] in <filename unknown>:0 
           at Microsoft.AspNet.Server.Kestrel.Disposable.Dispose (Boolean disposing) [0x00000] in <filename unknown>:0 
           at Microsoft.AspNet.Server.Kestrel.Disposable.Dispose () [0x00000] in <filename unknown>:0 
           at Microsoft.AspNet.Hosting.HostingEngine+<>c__DisplayClass9_0.<Start>b__1 () [0x00000] in <filename unknown>:0 
           at Microsoft.AspNet.Hosting.HostingEngine+Disposable.Dispose () [0x00000] in <filename unknown>:0 
           at AspNet5IntegrationTesting.Tests.CustomerControllerIntegrationTest.ShouldReturnCustomer () [0x00000] in <filename unknown>:0 
           at (wrapper managed-to-native) System.Reflection.MonoMethod:InternalInvoke (System.Reflection.MonoMethod,object,object[],System.Exception&)
           at System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <filename unknown>:0 
Finished:    AspNet5IntegrationTesting

=== TEST EXECUTION SUMMARY ===
   AspNet5IntegrationTesting  Total: 1, Errors: 0, Failed: 1, Skipped: 0, Time: 0.697s

If somebody has an idea about this, please send me a pull request at GitHub (yes, you can grab the source over there) ;-)

Update [2015/05/17]: This is a known Kestrel issue as described here (Description starts here) and a fix is on the way.

What are your thoughts about
"Integration-testing ASP.NET 5 / MVC 6 Controllers on DNX Beta 4"?
Drop me a line - I'm looking forward to your feedback!
Please be aware that I'm no longer active on social media. I'm just cross posting things over there (it's a bot).
Imprint | Privacy