Friday, February 5, 2010

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

Part 1: using HttpClient

In this post I describe how you can use HttpClient to write wire-first tests.
I’ll be discussing the service side of the world in part 2 of the post.

So you’ve added WebApiEnabled to your controller class, your unit tests continue to work but how do you know that the web api is also working as expected? Also, since your web api sets out to be compatible with an heterogeneous array of web clients, how do you verify that the wire format is the one I expect?

The approach I suggest here is to make use of the HttpClient API that is available for download on Codeplex as part of the WCF REST Starter Kit Preview 2 (get it here). The HttpClient API is an expressive and clean API that lets you write tests that are very concise, readable and easy to write and maintain.

So, let’s assume your controller looks like so:

[WebApiEnabled]
public class MoviesController : Controller {
    static List<Movie> Movies = new List<Movie> {
        new Movie { Id = 1, Title = "La vita รจ bella", Director = "Roberto Benigni", DateReleased = DateTime.Parse("20/12/1997") }, 
        new Movie { Id = 2, Title = "Avatar", Director = "James Cameron", DateReleased = DateTime.Parse("18/12/2009") }, 
        new Movie { Id = 3, Title = "The Godfather", Director = "Francis Ford Coppola", DateReleased = DateTime.Parse("20/12/1997") } 
    };

    // GET: /Movies/Details/5 
    public ActionResult Details(int id) {
        var movieToDisplay = (from m in MoviesController.Movies
                              where m.Id == id
                              select m).FirstOrDefault();
        if (movieToDisplay == null) {
            throw new HttpException((int)HttpStatusCode.NotFound, "No movie matching '" + id + "'");
        }
        return View(movieToDisplay);
    }
}

And suppose you’re hosting this at the “http://localhost/MyMvcApp” address (a lot more about this in Part 2), here’s what a test that verifies that asking for a non existent movie gets you a 404 response looks like:

[TestMethod]
public void Movies404() {
    HttpClient client = new HttpClient("http://localhost/MyMvcApp");
    using (HttpResponseMessage response = client.Get("Movies/9999")) {
        Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
    }
}

Because HttpClient is very easy to extend via extension methods, you can add one that lets you easily specify an Accept header, like so:

public static class HttpClientExtensions {
    public static HttpResponseMessage Get(this HttpClient client, string uri, string acceptContentType) {
        HttpRequestMessage request = new HttpRequestMessage("GET", uri);
        request.Headers.Accept.AddString(acceptContentType);
        return client.Send(request);
    }
}

And you can start testing the multi-format behavior that WebApiEnabled provides out of the box:

[TestMethod]
public void Movies404Xml() {
    HttpClient client = new HttpClient("http://localhost/MyMvcApp");
    using (HttpResponseMessage response = client.Get("Movies/9999", "text/xml")) {
        Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
        Assert.AreEqual("application/xml; charset=utf-8", response.Content.ContentType);
    }
}

And, of course, you can write more complex test cases that test the behavior of multiple related requests. Here’s an example testing that you can add an item usng the json format and then check that it was added using the xml format. It makes 3 subsequent requests:

[TestMethod]
public void MoviesCrudJsonXml() {
    MoviesComparer moviesComparer = new MoviesComparer();
    List<Movie> originalMovieList;
    HttpClient client = new HttpClient("http://localhost/MyMvcApp");
    using (HttpResponseMessage response = client.Get("Movies", "application/xml")) {
        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual("application/xml; charset=utf-8", response.Content.ContentType);
        originalMovieList = response.Content.ReadAsDataContract<List<Movie>>();
    }
    string director = "Nichols";
    DateTime dateReleased = new DateTime(1967, 12, 21);
    string title = "The Graduate";
    Movie movieToInsert = new Movie { Director = director, DateReleased = dateReleased, Title = title };
    using (HttpResponseMessage response = client.Post("Movies/Create", HttpContentExtensions.CreateJsonDataContract(movieToInsert))) {
        Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
        Assert.AreEqual("application/json; charset=utf-8", response.Content.ContentType);
    }
    List<Movie> updatedMovieList;
    using (HttpResponseMessage response = client.Get("Movies", "application/json")) {
        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual("application/json; charset=utf-8", response.Content.ContentType);
        updatedMovieList = response.Content.ReadAsJsonDataContract<List<Movie>>();
    }
    Movie insertedMovie = updatedMovieList.Except(originalMovieList, moviesComparer).SingleOrDefault();
    Assert.IsTrue(moviesComparer.Equals(movieToInsert, insertedMovie));
}

which uses the following comparer for the Movie class:

public class MoviesComparer : EqualityComparer<Movie> {
    public override bool Equals(Movie x, Movie y) {
        return x.Director == y.Director && x.Title == y.Title && x.DateReleased == y.DateReleased;
    }
    public override int GetHashCode(Movie obj) {
        return obj.Director.GetHashCode() ^ obj.Title.GetHashCode() ^ obj.DateReleased.GetHashCode();
    }
}

The technique used here is in fact the same technique we ended up using to write tests while developing the WebApiEnabled support itself. In the near future we’re looking at making the tests available for you to download.

0 comments: