Practical .NET

Test-Driven Development with Content Negotiation in the Web API

There's no doubt that the ASP.NET Web API is a wonderful thing. But developing services that support content negotiation in a testable way requires a little setup.

One of the best parts of creating either Windows Communication Foundation (WCF) or Web API services is that the underlying code is just code in a class. A complete Web API method that retrieves an object through the Entity Framework (EF) and returns a corresponding Data Transfer Object (DTO) might be as simple as the code shown in Listing 1, for instance.

Listing 1: A Simple, Testable Web API Service
Public Class MyWebApiService
  Inherits ApiController

  Public Function Get(id As String) As HttpResponseMessage 
  Dim cust As Customer
  Dim custDTO As New CustomerDTO

  Using oc As New CustomerEntities
    cust = (From cust In oc.Customers
            Where cust.Id = id
            Select oc).FirstOrDefault
    If cust IsNot Nothing Then
      custDTO.Id = cust.Id
      custDTO.FirstName = cust.FirstName
      more assignment statements... 
    End If
  End Using

  Dim hrm As HttpResponseMessage 
  hrm = Me.Request.CreateResponse(
        of Customer)(HttpStatusCode.OK, cust)
  Return hrm;

  End Function
End Class

It would be convenient if I could just test my service method with code like this:

<TestMethod>
Public Sub TestGetById()
  Dim mwas As New MyWebAPIService
  Dim hrm As HttpResponseMessage 
  hrm = mwas.Get("A123")
  Assert.AreEqual(hrm.StatusCode, HttpStatusCode.OK, 
                  "Wrong status code on get by Id");
End Sub

Unfortunately, I can't.

Testing with Content Negotiation
If I had just returned a collection of JSON objects from my Web API method, I would be able to catch the result of my test and check the result. Even if I just created and returned an HttpResponseMessage object (which lets me control the return code from my method), I could probably just write the test method as I showed it.

However, either of those strategies would limit my Web API method to working only with clients that want JSON objects (which, I grant you, would be most of the clients for a Web API service). But the Web API supports content negotiation: If a client asks for an XML response, the Web API is willing to send an XML response no matter what I return from my method. In order to give the Web API the flexibility to return my result in whatever format makes sense to the client, I have to use the CreateResponse method.

And, to be fair, it's not the CreateResponse method that's the problem: it's the ASP.NET Request object that's the issue. The Visual Studio Test environment doesn't provide a Request object by default and my code won't execute without it. There is a solution: Test my code by providing a Request object on my own. In the same way that I can create an HttpResponseMessage, I can create an HttpRequestMessage object that and tie it to the Web API service with the content negotiation method I want to test.

I could generate that HttpRequestMessage in every test method. It makes sense to me, however, to create that object and tie it to my service in a TestInitialize method. Using a TestInitialize method has two benefits. First, it factors some repetitive code out of my test methods. Second, it ensures the HttpRequestMessage is created fresh before each test -- this ensures my tests really are independent of each other.

In my test class initialize method, I need to instantiate both an HttpRequestMessage class and my Web API service class, and then set the Request property on my service class to the HttpRequestMessage object:

<TestClass>
Public Class CustomerManagementServerTests
  Dim mwas As MyWebApiService

  <TestInitialize>
  public Sub SetUpCustomerManagementController()
    Dim hrqm As HttpRequestMessage 
    hrqm = new HttpRequestMessage()
    mwas = new MyWebApiService
    mwas.Request = hrqm

That's not quite enough, however: I also have to configure my HttpRequestMessage object. Fortunately, the HttpRequestMessage object is satisfied with a default HttpConfiguration object added to the HttpRequestMessage Properties collection with the key HttpPropertyKeys.HttpConfiguration. That code, at the end of my initialize method, looks like this:

HttpConfiguration cfg = new HttpConfiguration()
cmc.Request.Properties(
  HttpPropertyKeys.HttpConfigurationKey) = cfg
End Sub

Extending the Test
Now I really can use the test method that I showed before. However, in addition to testing the response code in the HttpResponseMessage, I should check that the data returned in the body of the message (my Customer object) is also correct.

To extract the content of the HttpRequestMessage object, I use the ReadAsStringAsync method on the object Content property. That gives me a Task object holding a string. Reading the Task Result property causes my code to wait for the operation to complete and then gives me the resulting string:

Dim tsk As Task(of String)
tsk = hrm.Content.ReadAsStringAsync()
Dim res As String
res = tsk.Result

To get my Customer object, I pass that string to the JsonCovert class DeserializeObject method, specifying that I want to get a Customer object back:

Dim cust as Customer
cust = JsonConvert.DeserializeObject(of Customer)(res)

Now I can check to see if I got the right result:
Assert.AreEqual("A123", cust.Id, "Wrong customer retrieved on get");

I'll grant you that having to write the code to initialize your service with the HttpRequestMessage is a pain -- but, by putting that code in a TestInitialize method, you only have to write that code once. With that out of the way, you can write all the tests you need to prove that your Web API Service works the way you think it does.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube