In my last post, I talked about decoupling the construction of the OWIN pipeline from the definition of the OWIN middleware component. In this post, I'm going to talk about the next ingredient, the testing story.
Ingredient 4: Testing your entire HTTP pipelineIn most projects that are building ASP.NET MVC or WebAPI controllers, I've observed two testing strategies:
- Test the controller class directly by passing in strongly typed message objects and analyzing the resulting objects.
- Keep the controller code as thin as possible and test the underlying service code.
There used to be a WebAPI self-host that you could use from inside your unit test, but it still requires an actual network port. Although slow, it can work, provided that you don't run your unit tests in parallel (the default for XUnit 2).
The beauty of OWIN is that it doesn't have a direct dependency on an actual network stack. It just defines an abstraction based on simple types like dictionaries, tasks and funcs. It's Microsoft's Katana that provides an IIS host, a console host and even a Windows Service host. So ideally, your unit test could cover the entire OWIN pipeline without the need of real network ports, nor has any restrictions when they run in parallel with other unit tests. If you use OWIN, I wrote a unit test that asserts that Piercer returns its own assemblies when run from inside the unit test runner AppDomain like this.
[Fact]
public async Task When_specifying_an_explicit_route_it_should_be_reachable_through_that_route()
{
var appBuilder = new AppBuilder();
appBuilder.UsePiercer(new PiercerSettings().AtRoute("/myroute"));
AppFunc app = appBuilder.Build();
var httpClient = new HttpClient(new OwinHttpMessageHandler(appFunc));
var result = await httpClient
.GetStringAsync("http://localhost/myroute/piercer/assemblies");
result.Should().Contain("Piercer.Middleware");
}
Using an independent AppBuilder allows you to build an in-memory OWIN pipeline accessible through an AppFunc. To send actual HTTP requests into that pipeline from a typical HttpClient object, you can use a nifty little library created by Damian Hickey, the OwinHttpMessageHandler. It will convert the HttpRequestMessage objects that the HttpClient client sends into the dictionary that the AppFunc expects, all without ever touching a network stack. You can check out its implementation here.
So imagine you're writing a middleware component that needs to communicate with the outside world. I assume you would probably add a property or method to your middleware's settings class (like PiercerSettings) taking the URL to communicate with. Well, don't do that. Instead, either take an HttpClient or a Uri and an optional HttpMessageHandler instance. If you take that optional HttpMessageHandler, callers can either pass in the actual URL to connect to or the aforementioned OwinHttpMessageHandler like this:
appBuilder.UsePiercer(new PiercerSettings()
.ConnectingTo(new Uri("http://localhost"));
Or within unit tests:
appBuilder.UsePiercer(new PiercerSettings()
.ConnectingTo(new Uri("http://localhost", new OwinHttpMessageHandler(appFunc))
);
The definition of the ConnectingTo method might look like this:
public PiercerSettings ConnectingTo(Uri uri, HttpMessageHandler handler = null)
{
httpClient = new HttpClient(handler ?? new HttpClientHandler())
{
BaseAddress = uri
};
return this;
}
Now assume that within your unit test, the component that you're connecting to should also be hosted on the same AppBuilder. You can't pass in the AppFuncuntil you've completed building the pipeline, so we're at a kind of stand-off here. You can fix that by relying on the delayed execution of lambda expressions. Just redefine the ConnectingTo method so that it takes a Func:
public PiercerSettings ConnectingTo(Uri uri, FunchandlerFunc = null) { }
Then, you can do this:
AppFunc appFunc = null;
appBuilder.UsePiercer(new PiercerSettings()
.ConnectingTo(() => new OwinHttpMessageHandler(appFunc));
appBuilder.UseOtherService();
appFunc = appBuilder.Build();
The only caveat is that your middleware component shouldn't try to access the other service until the OWIN pipeline has been fully build. If that doesn't happen until your component is receiving its first HTTP request, you'll be fine. But if your component is doing some kind of background processing, you'll have to delay that explicitly, something I'll discuss in the next ingredient.
Leave a Comment