Friday, February 5, 2010

Wire-first testing WebApiEnabled applications using in-proc hosting and HttpClient (2)

Part 2: using in-proc hosting

In this post I describe how to host your web application/service in the test infrastructure for wire-first testing.
I’ve discussed the client side of the world in part 1 of the post.

In the previous part of this post we didn’t go into the details of hosting the applications and services that we set out to test. In fact there are two main issues with the approach we described: 1) it doesn’t help us start/stop the service when we do a test run 2) when we want to debug our tests, we are debugging the HttpClient code, not the server side code which is what we are trying to test (BTW, this also breaks code coverage). This post describes an approach that addresses these two issues.

Some services framework, such as WCF, allow you to host services in your own process. You can easily write tests for such a service using a Visual Studio Test Project: I usually would have a TestClass with an AssemblyInitialize method where I start the service and and AssemblyCleanup method where I stop the service. This would look more or less like this:

[TestClass]
public class TestCommon {
    static ServiceHost service;

    [AssemblyInitialize]
    public static void AssemblyInit(TestContext context) {
        service = new WebServiceHost(typeof(Service), new Uri("http://localhost/Temporary_Listen_Address/Service"));
        service.Open();
    }

    [AssemblyCleanup]
    public static void AssemblyCleanup() {
        service.Close();
    }
}

Very often, though, your service in production will be hosted in IIS/ASP.NET which makes the approach I just described less desirable as the test environment would be farther away from your production environment, and if you actually have a hard dependency on ASP.NET like you would have if you’re using WebApiEnabled (or in WCF if your AspNetCompatibilityRequirementsMode is set to Required), the approach will not work at all.

While hosting your service on Cassini or on the local IIS server will bring you much closer to the production environment, it makes testing more tedious because you might have to manually deploy to and start/stop those hosts manually, it also makes debugging very time consuming as you’ll have to manually attach to different processes.

The approach I describe below alleviates this problem and, while not being perfect, strikes a good balance of fidelity, in terms of emulating the production environment, and convenience, in terms of fast, easy and integrated the experience is for running and debugging tests. The key to the proposed approach is to host ASP.NET in the same process as the one that is used to run the tests and to hook the start/stop in the same way as we did above for WCF.

Not everyone knows that the Cassini ASP.NET host that Visual Studio uses, is a reusable component that can also run outside of Visual Studio itself, all we need is to write some infrastructure code to hook it all of this together. We will start by adding a reference to one of the Cassini assemblies in the test project. In Visual Studio 2008 SP1, the assembly is called WebDev.WebHost.dll and while it is in the GAC, I doesn’t show up in Visual Studio’s list of assembly references by default. While there might be a way to change that, I just add it manually by opening the .csproj file and adding the following to the list of references:

<Reference Include="WebDev.WebHost" />

We can now use an approach similar the one I describe above for WCF. Instead of using the ServiceHost class, we will be using the Server class in the Microsoft.VisualStudio.WebHost namespace.

[TestClass]
public class TestCommon {
    public static int AspPort = 59341; // just pick one 
    public static string AspVirtualPath = "/MyMvcApp";
    public static string AspBaseAddress = "http://localhost:" + AspPort + AspVirtualPath + "/";
    static Server server;

    [AssemblyInitialize]
    public static void AssemblyInit(TestContext context) {
        server = new Server(TestCommon.AspPort, TestCommon.AspVirtualPath, hostedRoot, false, false);
        server.Start();
    }

    [AssemblyCleanup]
    public static void AssemblyCleanup() {
        server.Stop();
    }
}

Now we can also change our HttpClient to use a matching addess, so these are always in sync in case we decide to change them. In fact we normally just have the client be a member of the TestClass, so all TestMethods can share it w/out having to create one each time:

HttpClient client = new HttpClient(TestCommon.AspBaseAddress);

There is, however, extra complexity that has to do with the fact that ASP .NET needs a physical directory from which to load files, so we need to write code to “find it”. In fact, you might have observed that i am using a string hostedRoot that I didn’t even define, here’s the code that I use to compute that:

// make relative to where TestResults are dropped
string hostedRoot = Path.Combine(context.TestDir, @"..\..");
// make relative to where the MvcRestTest folder is located
hostedRoot = Path.Combine(hostedRoot, @"/* web app project folder */");
hostedRoot = new DirectoryInfo(hostedRoot).FullName;

I certainly am not happy about how it depends on the physical layout of the folders in your solution, but it’s the best I could come up with.

Unfortunately the complexity is not over yet: in order to make code coverage work, we will need to write code to propagate the instrumented binaries that Visual Studio copies to the TestResults folder, to the bin folder under the web application project folder, the code looks like:

string hostedLocation = Path.Combine(hostedRoot, "bin");
string location = Path.GetDirectoryName(typeof(TestCommon).Assembly.Location);
TestCommon.MergeBinaries(location, hostedLocation);

Where the (pretty naive) implementation for MergeBinaries, looks like this:

static void MergeBinaries(string location, string hostedLocation) {
    List<string> sources = new List<string>();
    sources.AddRange(Directory.GetFiles(location, "*.dll"));
    sources.AddRange(Directory.GetFiles(location, "*.exe"));
    sources.AddRange(Directory.GetFiles(location, "*.pdb"));
    foreach (string source in sources) {
        string destination = Path.Combine(hostedLocation, Path.GetFileName(source));
        bool overwrite = false;
        if (File.Exists(destination)) {
            FileInfo s = new FileInfo(source);
            FileInfo d = new FileInfo(destination);
            if (s.CreationTime <= d.CreationTime) {
                continue;
            }
            overwrite = true;
        }
        File.Copy(source, destination, overwrite);
    }
}

That’s pretty much it, now we can hit Run Test, Debug Test, look at code coverage and it should all work and we should get a very nice integrated experience.

In AssemblyInit you can also add checks for pre-requisites of your tests, so if anything wrong is detected, you can fail early and avoid getting bogus results. For example I add a check that verifies that my mdf and ldf files exist in the expected location and that they are writeable, otherwise when I run tests that update the data in the database, they would fail and I would be left wondering whether I just introduced a regression.

P.S.: though I say applications in the title, you are much more likely to do wire-first testing for services. That said, keep in mind that this approach will work for applications too. Also, a lot of what I discuss here is actually applicable to a larger class of services, beyond ones that are built using WebApiEnabled, such as WCF SOAP, WCF REST and ASMX services.

0 comments: