5

So, I'm doing a project that needs to consume a REST service. I'm using C#, and I'm writing implementation myself with HttpClient class. I also try to create some Unit Testing for my library, but that class doesn't make it easy. Here are parts of the conflict:

  1. HttpClient is designed to be used as singleton, as indicated by, for example, this article.
  2. Testing interaction with a service should swap the dependecy on real service for a fake, since unit-test should be as independent as possible.
  3. HttpClient doesn't implement any interface, so you can't make your own implementation, instead you seem to be supposed to implement HttpMessageHandler to swap requests like here
  4. HttpClient is initialized with HttpMessageHandler and the used handler can't be changed at runtime.

I now struggle to implement the library with all that in mind. Right now I have Handler as a paramater in singleton GetInstance, however the parameter is only used if it's first call and ignored all other times, which feels like bad design and unexpected behaviour. Please advise.

  • 1
    What is stoping you to create your own interface? – Euphoric Jul 24 '18 at 11:56
  • its not designed to be used as a singleton. its designed to be reused for multiple requests – Ewan Jul 24 '18 at 12:04
  • Ewan, and how would you propose to reuse it without employing singleton pattern? – Misamoto Jul 24 '18 at 12:05
  • Euphoric, I'm only a beginner. While I've read some books on software design, without actual practice it's not substantial knowledge. If you have a suggestion on a good design feel free to add an answer – Misamoto Jul 24 '18 at 12:07
  • @Misamoto I believe what Euphoric talking about it have a wrapper of your dependent third-party class, then you can have interface. – ivenxu Jul 25 '18 at 3:31
7

A common way to get around such classes is to use the delegation pattern (not to be confused with delegates in C#), to create a wrapper around HttpClient.

For example, let's say you only use GetAsync and PostAsync. You then create a suitable interface:

public interface IHttpClient
{
    Task<HttpResponseMessage> GetAsync(string uri);
    Task<HttpResponseMessage> PostAsync(string uri, HttpContent content);
}

And then your runtime class looks something like:

public class HttpClientWrapper : IHttpClient
{
    private HttpClient _client = new HttpClient();

    public async Task<HttpResponseMessage> GetAsync(string uri)
        => await _client.GetAsync(uri);

    public async Task<HttpResponseMessage> PostAsync(string uri, HttpContent content)
        => await _client.PostAsync(uri, content);
}

For testing, you then just create a mock implementation of IHttpClient.

Obviously, if you use more methods and properties of HttpClient, then you expand IHttpClient accordingly.

4

Don't unit test your Client library.

The Client should only be concerned with communicating with a server. Testing it in isolation isn't very helpful.

Write Integration tests for the Client + Server and Unit tests for the service behind the hosting layer.

3

HttpClient doesn't implement any interface, so you can't make your own implementation

That's true.

However, what you can do, is introduce abstractions of your own, that are implemented using the HttpClient. In your tests -- where you want the test to be isolated from the real world -- you create a test double for your own abstraction.

The motivation here is analogous to the decomposition rules described by Parnas: you have made a decision to use an implementation that depends on HttpClient. To isolate the rest of your program from that decision, you wrap it in a module boundary. For your tests, where you want to make a different decision, you have a different module. By arranging that both of these modules have the same API surface, you allow one to be used in place of the other (Liskov)

1

If i where you i would

  • create a c# interface with all the business logic that you lib provides that has no dependency to an web/http/session
  • implement all aspects of the logigic independant of web/http/session that implements your interface
  • implement a http-transport version of the same interface that encapsulates all web/http/session and forwards all business processing to the same interface. this is a thin http transport specific wrapper around your logic

this way you can unit test the buisness logic without any http. All comunication of lib clients only go through the interface

try to implement

0

Since HttpClient is used as a singleton, you would register it in your (normal, production) IoC container like this (depending on what container you're using-- I'm using AutoFac):

public Container CompositionRoot()
{
    var builder = new ContainerBuilder();
    container.RegisterSingleton<HttpClient>();
    container.RegisterSingleton<HttpClientHandler>();
    return builder.Build();
}

Because you registered the default HttpClientHandler, the production code will access the network, as required.

In your test project, you just need to register it differently:

public class MyHandler : System.Net.Http.HttpClientHandler
{
    public bool WasCalled = false;

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        WasCalled = true;
        return new HttpResponseMessage
        {
            Content = new StringContent("This is a test response.")
        };
    }
}

public Container CompositionRoot()
{
    var builder = new ContainerBuilder();
    container.RegisterSingleton<HttpClient>();
    container.RegisterSingleton<MyHandler>().As<HttpClientHandler>();
    return builder.Build();
}

Or inject it directly:

//Arrange
var o = new ClassUnderTest(new HttpClient(new MyHandler()));

//Act
o.DoSomethingThatNeedsTheHttpClient();

//Assert
Assert.IsTrue(o.WasCalled);

Once you register (or inject) the MyHandler class, it will automatically get injected into the HttpClient when the object graph is composed, and your test code will run, allowing you to isolate away the network access.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.