Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / C#

FakeHttp

Rate me:
Please Sign up or sign in to vote.
4.84/5 (17 votes)
25 Oct 2015CPOL13 min read 41.3K   31   13
Faking http response messages to decouple client unit tests from service implementation
This library should come in handy for unit testing any code that uses the System.Net.Http.HttpClient component. It is designed to be minimally intrusive to existing code, making it relatively easy to fake http traffic without major changes to already written components or tests.

Introduction

When writing unit test code, it is important to isolate the code under test, to the greatest extent possible, in order to ensure that the tests are atomic. The more things a single unit test is dependant on, the higher the likelihood that success or failure will become unrelated to the intent of the test.

When writing the Dynamic Rest Client for example, I created some unit tests that used Bing's Locations Rest API. These tests would fail on occasion in such a way that when they failed, if I ran them immediately afterwards, they would succeed. This was very difficult to figure out and it was ultimately caused by how the Bing service responded when the service was very busy and had nothing to do with my rest client at all. This ultimately lead me to create this library so that I could fake the service response to ensure that what was being unit tested was the code under test and nothing more.

Now creating http clients is not something that we do every day but writing client side rest service wrappers is pretty common. This library should come in handy for unit testing any code that uses the System.Net.Http.HttpClient component. It is designed to be minimally intrusive to existing code, as is described below, making it relatively easy to fake http traffic without major changes to already written components or tests.

Background

The faking itself is implemented by an HttpMessageHandler and the only thing that a component needs to ensure in order to be compatible with FakeHttp is that it accept, rather than create, a handler when instantiating an HttpClient. This can be handled with a factory method or object, the IoC container of your choice, or a simple constructor parameter.

So code that may have looked like this:

C#
public class GeoCoder : IGeoCoder
{
    private readonly HttpClient _httpClient;

    public GeoCoder()
    {
        _httpClient = new HttpClient(new HttpClientHandler(), true);
        _httpClient.BaseAddress = 
             new Uri("http://dev.virtualearth.net/REST/v1/", UriKind.Absolute);
    }

    ...
}

Should look like this:

C#
public class GeoCoder : IGeoCoder
{
    private readonly HttpClient _httpClient;

    public GeoCoder(HttpMessageHandler handler)
    {
        _httpClient = new HttpClient(handler, false); // flag controls disposal of the handler 
        _httpClient.BaseAddress = new Uri("http://dev.virtualearth.net/REST/v1/", 
                                           UriKind.Absolute);
    }

    ...
}

Once that's done, the above code can be passed a FakeHttpMessageHandler from a unit test or IoC container and be none the wiser that it isn't connected to a service. The FakeHttpMessageHandler is a pretty simple object whose only job is to bybass the network and get response messages from some alternate storage:

C#
/// <summary>
/// A <see cref="System.Net.Http.HttpMessageHandler"/> that retrieves 
/// http response messages from
/// an alternate storage rather than from a given http endpoint
/// </summary>
public sealed class FakeHttpMessageHandler : HttpMessageHandler
{
    private readonly IReadonlyResponseStore _store;

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="store">The storage mechanism for responses</param>
    public FakeHttpMessageHandler(IReadonlyResponseStore store)
    {
        _store = store;
    }

    /// <summary>
    /// Override the base class to skip http and retrieve message from storage
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns>The stored response message</returns>
    protected async override Task<HttpResponseMessage> 
              SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        return await _store.FindResponse(request);
    }
}

Response Storage

By default, responses are stored on the local file system in a folder structure that mirrors the end point URL. So the endpoint http://dev.virtualearth.net/REST/v1/Locations is going to be stored in a folder something like: d:\Users\Don\Documents\GitHub\FakeHttp\FakeResponses\dev.virtualearth.net\REST\v1\Locations. (Note: Currently, there is no logic to ensure that the Uri path can be represented as a valid file path. In practice, this is typically the case so I haven't needed to do any character mapping other than replacing slashes with back slashes).

For the example unit tests that go along with the Bing Locations service, you'll see these files:

Image 1

Each response has one or two files, one for the response itself (a serialized version of an HttpResponseMessage) and another for the response content. Responses are serialized as json while content is serialized as a stream directly from the service, with a file extension derived from the content type of the response. Most of the time, response content will be JSON but XML, HTML or other content types are supported as well.

Any given faked endpoint has to have one, the other or both of these files. If the response headers aren't important, only a content file can be provided. In this case, the response will return OK with the content attached, and empty header collections. When the response is retrieved and reconstructed, these two files represent what is returned by the FakeHttpMessageHandler. The content file is serialized exactly as it is received, while the serialized response is a json file that might look like this:

JavaScript
{
  "StatusCode": 200,
  "Query": "c=en-us&countryregion=us&maxres=1&postalcode=55116",
  "ContentFileName": "GET.8F8BE39FAED23347CB3B40A0053E1EA46644AB9C.content.json",
  "ResponseHeaders": {
    "Transfer-Encoding": [
      "chunked"
    ],
    "X-BM-TraceID": [
      "d2363ee13d844c9cbaa2db37abf6688b"
    ],
    "X-BM-Srv": [
      "BN20121762, BN2SCH020180739"
    ],
    "X-MS-BM-WS-INFO": [
      "0"
    ],
    "Cache-Control": [
      "no-cache"
    ],
    "Date": [
      "Mon, 06 Jul 2015 20:18:09 GMT"
    ],
    "Server": [
      "Microsoft-IIS/8.0"
    ],
    "X-AspNet-Version": [
      "4.0.30319"
    ],
    "X-Powered-By": [
      "ASP.NET"
    ]
  },
  "ContentHeaders": {
    "Content-Type": [
      "application/json; charset=utf-8"
    ]
  }
}

The file name for each response is generated deterministically based on the end point and the url parameters used to call it. The format of the filename is as follows: "Http Verb"."SHA1 hash"."(response | content)" If no url parameters are needed at the endpoint, the SHA1 hash will be empty resulting in a name like "GET.response.json".

Parameter Hashing

In order to work, a given combination of endpoint path, verb and query parameters always needs to result in the same response. The hash assumes that parameters are not case sensitive nor order dependant. Its implementation is in the MessageFormatter class in the linked code but is relatively straightforward:

  1. Filter out user defined ignore list parameters (see below)
  2. Order the parameters alphabetically
  3. Concat them to a single string in canonical Uri parameter list format
  4. ToLower that string
  5. SHA1 hash the result

There are services that take parameters in the request body as opposed to on the Uri, or as a combination of Uri query parameters and data in the request body. Request body parameterization isn't currently supported, but is on my todo list.

Using the Code

As long as the code under test accepts an HttpMessageHandler, everything else is done on the unit test side. The code under test does not need any reference to the FakeHttp types or assembly. For the examples below, I'm using MSTest. The same concepts apply to other unit test frameworks, though the exact mechanisms will be different. I'm also using the SimpleIoC container that is part of the MVVM Light library. Again, the same concepts apply to other containers.

Automatic Response Management

The quickest way to get up and running with FakeHttp is to use the AutomaticHttpClientHandler anywhere you instantiate an HttpClient. This handler will first check whether a response is locally stored for each request. If it is, the response is returned. If no local response is found, the handler will contact the actual endpoint. It then stores the response and returns it to the client. To force a refresh of the response data, simply delete the local versions and they will be fetched from the online endpoints.

C#
[TestMethod]
public async Task CanAccessGoogleStorageBucket()
{
    // this is the path where responses will be stored for future use
    var path = Path.Combine(Path.GetTempPath(), "FakeHttp_UnitTests");
    
    var handler = new AutomaticHttpClientHandler(new FileSystemResponseStore(path));
    
    using (var client = new HttpClient(handler, true))
    {
        client.BaseAddress = new Uri("https://www.googleapis.com/");
        using (var response = await client.GetAsync("storage/v1/b/uspto-pair"))
        {
            response.EnsureSuccessStatusCode();

            dynamic metaData = await response.Content.Deserialize<dynamic>();

            // we got a response and it looks like the one we want
            Assert.IsNotNull(metaData);
            Assert.AreEqual("https://www.googleapis.com/storage/v1/b/uspto-pair", 
                             metaData.selfLink);
        }
    }
}

Managing Response Files

It is also possible to explicitly control whether to use local or online versions of responses.

Since responses are serialized to the file system, it is necessary to make them accessible by the unit tests. This can be done in a few different ways; making them as content files of the unit test project, a path stored in a config file, environment variable, hard coded path or any number of other approaches. I've opted for a combination of a folder in the same directory as the solution, a build event and MSTest's DeploymentItem attribute. This approach is relatively straightforward and doesn't require ongoing management as responses and tests are added and removed.

  1. Create a folder in the same directory as the .sln file (FakeResponses in our example) where serialized responses will be kept.
  2. Add a pre-build event to the unit test project that will copy the fake responses to the test assembly output folder:
    del /f /s /q "$(TargetDir)FakeResponses\"
    xcopy /Y /S /Q "$(SolutionDir)FakeResponses\*" "$(TargetDir)FakeResponses\"
  3. Make sure that all test classes are decorated with [DeploymentItem(@"FakeResponses\")]. This can be at the test method or class level, but is easier to just put it on each test class. This attribute signals MSTest to copy the FakeResponses folder that was copied to the build output folder in Step 2, to wherever the tests will be executed. This way, it works both from VS.NET and on a build machine.

A Minimal Example

Once a reference to the NuGet package has been added to your unit test assembly, to setup up a minimal test endpoint, create the folder structure in the FakeResponses folder to match the endpoint path. We'll use a pretend endpoint http://www.example.com/HelloWorldService which would reside in a path that might look like: d:\Users\Don\Documents\GitHub\FakeHttp\FakeResponses\www.example.com\HelloWorldService.

Then, we'll drop a very simple content json file into that folder. It will be named GET.content.json because no query parameters will be simulated at this endpoint. Also, since we don't care about any specifics of the response, we won't create a response.json file.

JavaScript
{
	"Message": "Hello World"
}

With a fake endpoint in place, we can test it:

C#
[TestClass]
[DeploymentItem(@"FakeResponses\")]
public class ExampleTests
{
    public TestContext TestContext { get; set; }

    [TestMethod]
    public async Task MinimalExampleTest()
    {
        var handler = new FakeHttpMessageHandler
                      (new FileSystemResponseStore(TestContext.DeploymentDirectory));
        using (var client = new HttpClient(handler, true))
        {
            client.BaseAddress = new Uri("https://www.example.com/");
            var response = await client.GetAsync("HelloWorldService");
            response.EnsureSuccessStatusCode();

            dynamic content = await response.Content.Deserialize<dynamic>();

            Assert.IsNotNull(content);
            Assert.AreEqual("Hello World", content.Message);
        }
    }
}

Of course, that test truly is fake but demonstrates the minimal amount of plumbing necessary.

Setup for Capture and Playback

For large sets of unit tests or to support Capture and Playback, setting up the handler should be moved to some initialization methods. The first step is to setup an IoC container so that all of the unit tests can use the same message handler (note: IoC isn't really necessary in these simple examples but do become more valuable in more complex scenarios). With MSTest, we'll leverage its ability to mark methods to be called once for a unit test run.

C#
[AssemblyInitialize]
public static void AssemblyInitialize(TestContext context)
{
    // setup IOC so test classes can get the shared message handler
    ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

    // folders where fake responses are stored and where captured response should be saved
    var fakeFolder = context.DeploymentDirectory;  // the folder where 
                                                   // the unit tests are running
    var captureFolder = 
        Path.Combine(context.TestRunDirectory, @"..\..\FakeResponses\"); // kinda hacky 
                                                   // but this should be the solution folder

    // here, we don't want to serialize or include our API key in response lookups so
    // pass a lambda that will indicate to the serializer to filter that param out
    var store = new FileSystemResponseStore(fakeFolder, captureFolder, 
      (name, value) => name.Equals("key", StringComparison.InvariantCultureIgnoreCase));

    // set the http message handler factory to the mode we want for the 
    // entire assembly test execution
    MessageHandlerFactory.Mode = MessageHandlerMode.Fake;
    SimpleIoc.Default.Register<HttpMessageHandler>
               (() => MessageHandlerFactory.CreateMessageHandler(store));
}

[AssemblyCleanup]
public static void AssemblyCleanup()
{
    if (SimpleIoc.Default.IsRegistered<HttpMessageHandler>())
    {
        SimpleIoc.Default.GetInstance<HttpMessageHandler>().Dispose();
    }
}

There should be only one AssemblyInitialize and AssemblyCleanup per unit test assembly. The above initialization method does the following:

  1. Sets up the IoC container
  2. Figures out where serialized responses are stored for the test execution and where to serialize responses while capturing them
  3. Sets up a store object that knows how to store and retrieve responses
  4. Configures the execution mode
  5. Registers a factory method with the IoC for the HttpMessageHandler type (see Execution Modes below)

For the example, Bing Locations unit tests each test class has an instance of a service wrapper which is setup when the test class gets instantiated, grabbing the HttpMessageHandler from the IoC container and creating a GeoCoder service wrapper.

C#
[TestClass]
[DeploymentItem(@"FakeResponses\")]
public class AddressPartTests
{
    private static IGeoCoder _service;

    [ClassInitialize]
    public static void ClassInitialize(TestContext context)
    {
        var handler = SimpleIoc.Default.GetInstance<HttpMessageHandler>();

        _service = new GeoCoder(handler, CredentialStore.RetrieveObject("bing.key.json").Key, 
                   "Portable-Bing-GeoCoder-UnitTests/1.0");
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        if (_service != null)
        {
            _service.Dispose();
        }
    }
    
    ...
    
}

Execution Modes

So how does one go about crafting the serialized response and content files? Well, these could of course be hand crafted in any plain old text editor but that would be tedious to say the least. To ease this process, FakeHttp supports the recording and play back of the http traffic. The basic pattern being:

  1. Write a unit test that hits the actual service endpoint.
  2. Execute that unit test, capturing the response and content, serializing them to disk.
    • Optionally, manually modify the serialized response or content file to simulate the specific test conditions desired.
  3. Execute future tests, using the captured response files instead of the service response.

To facilitate easily switching between those modes, there is a static MessageHandlerFactory class which can be told to operate in one of three modes:

Automatic

In Automatic mode, CreateMessageHandler will return an instance of an AutomaticHttpClientHandler. This handler will use automatically store and return responses if they are not locally cached.

Online

In Online mode, CreateMessageHandler will return a standard HttpClientHandler instance. Unit test code will interact directly with whatever service is at the other end of the Uri. This is no different than using HttpClient as you normally would and no FakeHttp objects are taking part in communication.

Capture

In Capture mode, a handler is created that will still communicate with the service endpoint, but before returning the response, it will serialize it and its content to the file system for future use. It's after this point that you can edit the Json, perhaps changing the response status code, adding or removing headers, or modify the content file to contain specific values or data different than what the service responded with.

Information leak warning: Bear in mind that the response and content are serialized as is, to whatever folder you specify. This does open the risk for leaking information. If the service returns personal or sensitive data, it will be written to disk, so exercise some care depending on the nature of the service you are working with. See the section below about masking data if there is information that shouldn't be saved while capturing server responses.

Fake

This is the mode that your units tests will spend most of their time in. In this mode, contact is not made with the service, and responses are deserialized from what was stored while in Capture mode.

Now the logic for determining how the unit tests will run is all set, the unit tests themselves become very straightforward; focusing only on the code under test and the expected test results. If, for some reason such as a change in service behavior, it becomes necessary to execute tests against the actual service, simply switching to Online mode makes that a simple thing to do.

C#
[TestMethod]
public async Task GetNeighborhoodFromCoordinate()
{
    var address = await _service.GetAddressPart
                  (44.9108238220215, -93.1702041625977, "Neighborhood");

    Assert.AreEqual("Highland", address);
}

Controlling Fake Responses at Runtime

Under certain scenarios, you might need finer grained control over how responses are serialized, deserialized and indexed. For this purpose, there is a call back interface that can be supplied to the response store at the time of its construction.

public interface IResponseCallbacks
{
    Task<Stream> Deserialized(ResponseInfo info, Stream content);

    Task<Stream> Serializing(HttpResponseMessage response);

    bool FilterParameter(string name, string value);
}

Parameter Filtering

There are certain query parameters that you may not want to be part of the file name hashing. API keys are a good example of this. You don't want to hash those because they are not really part of the endpoint itself. Also, they may change from developer to developer and you wouldn't want the fake responses to be tied to a particular developer or team. A further point about API keys is that all of the query parameters are serialized in the response json. If you include API keys in the response serialization, they may leak out of your control if you unintentionally share the serialized response.

For this reason, there is a mechanism whereby API key parameters or other ephemeral parameter types can be excluded from both hashing and serialization. The FilterParameter that is passed both the parameter name and value can be used to suppress parameters. Return true from this function for any parameter that should be filtered out.

Masking Data Prior to Serialization

Since responses are serialized to disk, it is important to be cognizant that any sensitive data returned from a service may be visible in clear text. For instance, if you were testing against a service endpoint that included credit card or social security numbers, you would not want to store those on disk. The Serializing method is called just prior to saving the response and allows calling code to provide an alternate response stream.

C#
public async override Task<Stream> Serializing(HttpResponseMessage response)
{
    if (response.RequestMessage.RequestUri.Host == "www.googleapis.com")
    {
        // get the service content
        var result = await response.Content.Deserialize<dynamic>();

        // modify it
        result.storageClass = "THIS VALUE MASKED";

        // serialize and return a new stream which will be written to disk
        var json = JsonConvert.SerializeObject(result);
        return new MemoryStream(Encoding.UTF8.GetBytes(json));
    }

    return await base.Serializing(response);
}

Modifying Responses after Deserialization

And lastly, there may be instances where a time stamp or other temporarily sensitive values need to be set for the purposes of a particular test. The Deserialized method is called just after the response and content are re-hydrated and allows both to be modified prior to returning them to the HttpClient.

C#
public async override Task<Stream> Deserialized(ResponseInfo info, Stream content)
{
    if (info.ResponseHeaders.ContainsKey("Date"))
    {
        info.ResponseHeaders["Date"] = 
             new List<string>() { DateTimeOffset.UtcNow.ToString("r") };
    }
    return await base.Deserialized(info, content);
}

Points of Interest

Another case where response faking is particularly valuable is while testing how client code reacts to fault conditions or uncommon response logic in the service. This is especially true when the service in question is not under your control and you can't very well ask for an endpoint or instance that returns failure messages.

Going back to Bing Locations services, when the server is overloaded, it does not return a ServiceUnavailable or RequestTimeout status code. The Bing Locations service returns OK, a valid but empty JSON response and inserts the response header "X-MS-BM-WS-INFO" with a value of 1. This indicates the service is busy but you can retry the request. With faking, this response condition can be constructed so that retry logic can be reliably unit tested.

This is just one example of client side logic, dependent on service responses, that can be tested without needing to induce the response on the server side. I am sure there are others.

Another nice side effect of using fakes for this sort of thing is that the speed of units tests improves dramatically. In my experience, tests that might take hundreds of milliseconds round tripping over the web, take less than 10 ms with fakes. For lots of tests, that keeps execution time to the point where they can be run often and always.

History

  • 12th July, 2015 - Initial version
  • 24th July, 2015 - Added section about callbacks (v1.1.0)
  • 25th October, 2015 - Added section about automatic mode

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader Starkey Laboratories
United States United States
The first computer program I ever wrote was in BASIC on a TRS-80 Model I and it looked something like:
10 PRINT "Don is cool"
20 GOTO 10

It only went downhill from there.

Hey look, I've got a blog

Comments and Discussions

 
Generalvery useful! Pin
Southmountain25-Oct-15 15:23
Southmountain25-Oct-15 15:23 
thanks for sharing this.
diligent hands rule....

GeneralRe: very useful! Pin
Don Kackman26-Oct-15 3:20
Don Kackman26-Oct-15 3:20 
GeneralRe: very useful! Pin
GropenD4-Nov-15 2:27
GropenD4-Nov-15 2:27 
GeneralRe: very useful! Pin
Don Kackman5-Nov-15 2:43
Don Kackman5-Nov-15 2:43 
GeneralHave some query regarding angularjs Confirm-password validation Pin
Member 1183792728-Jul-15 19:42
Member 1183792728-Jul-15 19:42 
QuestionGood article Pin
Thomas Levesque15-Jul-15 9:25
professionalThomas Levesque15-Jul-15 9:25 
AnswerRe: Good article Pin
Don Kackman16-Jul-15 9:53
Don Kackman16-Jul-15 9:53 
GeneralRe: Good article Pin
Thomas Levesque20-Jul-15 4:08
professionalThomas Levesque20-Jul-15 4:08 
GeneralRe: Good article Pin
Don Kackman21-Jul-15 9:28
Don Kackman21-Jul-15 9:28 
GeneralDon is cool Pin
alex_zero12-Jul-15 23:08
alex_zero12-Jul-15 23:08 
GeneralRe: Don is cool Pin
Don Kackman13-Jul-15 3:32
Don Kackman13-Jul-15 3:32 
QuestionAnother gem Pin
Sacha Barber12-Jul-15 20:26
Sacha Barber12-Jul-15 20:26 
AnswerRe: Another gem Pin
Don Kackman13-Jul-15 3:33
Don Kackman13-Jul-15 3:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.