OWIN/Katana in-memory integration testing

Β· 817 words Β· 4 minutes to read

A while ago we looked at testing the ASP.NET Web API pipeline using its in-memory hosting capabilities.

The advantages of such approach to end-to-end testing are unquestionable.

Now, with the emergence of OWIN as the primary hosting option for a wide array of web frameworks, it makes sense to explore how you could to the same, except in the OWIN (or rather Katana, since all the things shown here are Katana specific) context - so not just against Web API, but against any framework of your choice (running on top of OWIN & Katana).

Creating dummy app to test πŸ”—

Suppose you have a simple Owin app - in our case I’ll pull in Nancy and ASP.NET Web API and use them to create some testable endpoints.

install-package Nancy.Owin  
install-package Microsoft.AspNet.WebApi.OwinSelfHost  

We’ll need a dummy ApiController and a dummy NancyModule:

public class TestController : ApiController  
{  
public IEnumerable<string> Get()  
{  
return new[] { "hello", "world" };  
}

public string Get(int id)  
{  
return "hello world";  
}  
}

public class HelloModule : NancyModule  
{  
public HelloModule()  
{  
Get["/"] = _ => "hello nancy";  
}  
}  

And, of course, this being an Owin/Katana app, let’s create a Startup class, where we configure the Web API routes and give it a priority over Nancy (the rationale is API routes should be more greedy):

public class Startup  
{  
public void Configuration(IAppBuilder appBuilder)  
{  
var config = new HttpConfiguration();  
config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional });

appBuilder.UseWebApi(config).UseNancy();  
}  
}  

Now we can proceed to writing integration tests for this.

Owin Integration tests πŸ”—

The easiest way to get started with OWIN integration tetsing is to install the Microsoft.Owin.Testing package from Nuget.

install-package Microsoft.Owin.Testing  

This will allow us to create an in-memory server, which will accept HTTP requests and issues HTTP responses without touching the network stack (entirely in process), which is both very efficient and extremely fast.

Note: if you do the tests in a different assembly from your web app (which I assume you do), and you use Web API or SignalR, you will need to add assembly binding redirects to your app.config). They are built with Microsoft.Owin 2.0.0 but the latest on Nuget is 2.0.2.

<runtime>  
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">  
<dependentAssembly>  
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />  
<bindingRedirect oldVersion="0.0.0.0-2.0.2.0" newVersion="2.0.2.0" />  
</dependentAssembly>  
</assemblyBinding>  
</runtime>  

In these examples we will use nunit but of course any test framework of your preference is fine.

[TestFixture]  
public class MyAppIntegrationTests  
{  
private TestServer _server;

[TestFixtureSetUp]  
public void FixtureInit()  
{  
_server = TestServer.Create<Startup>();  
}

[TestFixtureTearDown]  
public void FixtureDispose()  
{  
_server.Dispose();  
}

[Test]  
public void WebApiGetAllTest()  
{  
var response = _server.HttpClient.GetAsync("/api/test").Result;  
var result = response.Content.ReadAsAsync<IEnumerable<string>>().Result;

Assert.AreEqual(2, result.Count());  
Assert.AreEqual("hello", result.First());  
Assert.AreEqual("world", result.Last());  
}

[Test]  
public void WebApiGetTest()  
{  
var response = _server.HttpClient.GetAsync("/api/test/1").Result;  
var result = response.Content.ReadAsAsync<string>().Result;

Assert.AreEqual("hello world", result);  
}

[Test]  
public void NancyGetHelloTest()  
{  
var response = _server.HttpClient.GetAsync("/").Result;  
var result = response.Content.ReadAsStringAsync().Result;

Assert.AreEqual("hello nancy", result);  
}  
}  

So what are we doing here? We spin up an instance of an in-memory test server per test fixture, and allow each test to use it to make HTTP calls through the OWIN pipeline. Then in the tear down, we get rid of it.

We can make calls to individual endpoints of our web application by simply using the HttpClient that hangs off the server, and consume it the same way as you would consume any HTTP service in .NET - and then perform the necessary asserts. By the way - the ReadAsAsync extension method comes from the Microsoft.AspNet.WebApi.Client Nuget package and is used here merely for deserialization convenience.

passedtests

If, for some reason, you don’t want to use the “built in” HttpClient property, you can instantiate your own HttpClient very easily:

[Test]  
public void WebApiGetAllTest()  
{  
var client = new HttpClient(_server.Handler)  
{  
BaseAddress = new Uri("https://www.strathweb.com")  
};

var response = client.GetAsync("/api/test").Result;  
var result = response.Content.ReadAsAsync<IEnumerable<string>>().Result;

Assert.AreEqual(2, result.Count());  
Assert.AreEqual("hello", result.First());  
Assert.AreEqual("world", result.Last());  
}  

In this case we create a test client with a dummy absolute BaseAddress (it’s mandatory) and pass in the HttpMessageHandler hanging off the TestServer to the client’s constructor. This syntax and approach to working with the client might be familiar to you, because that’s exactly how we worked with integration tetsing in the traditional ASP.NET Web API in-memory hosting.

Either way, initializing and working with the in memory server is extremely straight forward. You may prefer to isolate it entirely and initialize the test server in every unit test but as far as I am concerned, the per-fixture approach is fine too. In that case you would simply dispose it per test:

using (var server = TestServer.Create<Startup>())  
{  
//do stuff with server  
}  

Interesting additional functionality is that you don’t even have to use your Startup class, as the entire config can be done inline using a lambda:

using (var server = TestServer.Create(app => {  
var config = new HttpConfiguration();  
config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional });  
app.UseWebApi(config).UseNancy();  
}))  
{  
//do stuff with server  
}  

Have at it, my friends!

About


Hi! I'm Filip W., a cloud 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