dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.18k stars 4.72k forks source link

An simple way to mock an httpClient.GetAsync(..) method for unit tests? #14535

Closed PureKrome closed 4 years ago

PureKrome commented 9 years ago

System.Net.Http has now been uploaded to the repo :smile: :tada: :balloon:

Whenever I've used this in some service, it works great but makes it hard to unit test => my unit tests don't want to actually ever hit that real end point.

Ages ago, I asked @davidfowl what should we do? I hoping I paraphrase and don't misquote him - but he suggested that I need to fake up a message handler (ie. HttpClientHandler), wire that up, etc.

As such, I ended up making a helper library called HttpClient.Helpers to help me run my unit tests.

So this works ... but it feels very messy and .. complicated. I'm sure I'm not the first person that needs to make sure my unit tests don't do a real call to an external service.

Is there an easier way? Like .. can't we just have an IHttpClient interface and we can inject that into our service?

sharwell commented 9 years ago

HttpClient is nothing more than an abstraction layer over another HTTP library. One of its primary purposes is allowing you to replace the behavior (implementation), including but not limited to the ability to create deterministic unit tests.

In other works, HttpClient itself serves as the both the "real" and the "mock" object, and the HttpMessageHandler is what you select to meet the needs of your code.

PureKrome commented 9 years ago

But if feels like sooo much work is required to setup the message handler when (it feels like) this could be handled with a nice interface? Am I totally misunderstanding the solution?

SidharthNabar commented 9 years ago

@PureKrome - thanks for bringing this up for discussion. Can you please elaborate on what you mean by "so much work is required to setup the message handler"?

One way to unit test HttpClient without hitting the network is -

  1. Create a new Handler class (e.g. FooHandler) that derives from HttpMessageHandler
  2. Implement the SendAsync method as per your requirements - don't hit the network/log the request-response/etc.
  3. Pass an instance of this FooHandler into the constructor of the HttpClient: var handler = new FooHandler(); var client = new HttpClient(handler);

Your HttpClient object will then use your Handler instead of the inbuilt HttpClientHandler.

Thanks, Sid

PureKrome commented 9 years ago

Hi @SidharthNabar - Thank you for reading my issue, I really appreciate it also.

Create a new Handler class

You just answered my question :)

That's a large dance to wiggle too, just to ask my real code to not hit the network.

I even made a the HttpClient.Helpers repo and nuget package .. just to make testing easier! Scenario's like the Happy path or an exception thrown by the network endpoint ...

That's the problem -> can we not do all of this and ... just a mock a method instead?

I'll try and explain via code..

Goal: Download something from the interwebs.

public async Foo GetSomethingFromTheInternetAsync()
{
    ....
    using (var httpClient = new HttpClient())
    {
        html = await httpClient.GetStringAsync("http://www.google.com.au");
    }
    ....
}

Lets look at two examples:

Given an interface (if it existed):

public interface IHttpClient
{
    Task<string> GetStringAsync(string requestUri);    
}

My code can now look like this...

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient()) // <-- CHANGE dotnet/corefx#1
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

and the test class

public async Task GivenAValidEndPoint_GetSomethingFromTheInternetAsync_ReturnsSomeData()
{
    // Create the Mock : FakeItEasy > Moq.
    var httpClient = A.Fake<IHttpClient>();

    // Define what the mock returns.
    A.CallTo(()=>httpClient.GetStringAsync("http://www.google.com.au")).Returns("some html here");

    // Inject the mock.
    var service = new SomeService(httpClient);
    ...
}

Yay! done.

Ok, now with the current way...

  1. create a new Handler class - yes, a class!
  2. Inject the handler into the service
public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null 
                                    ? new HttpClient()
                                    : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

But the pain is now that I have to make a class. You've now hurt me.

public class FakeHttpMessageHandler : HttpClientHandler
{
 ...
}

And this class starts out pretty simple. Until I when I have multiple GetAsync calls (so i need to provide multiple Handler instances?) -or- multiple httpClients in a single service. Also, do we Dispose of the handler or reuse it if we're doing multiple calls in the same block of logic (which is a ctor option)?

eg.

public async Foo GetSomethingFromTheInternetAsync()
{
    string[] results;
    using (var httpClient = new HttpClient())
    {
        var task1 = httpClient.GetStringAsync("http://www.google.com.au");
        var task2 = httpClient.GetStringAsync("http://www.microsoft.com.au");

        results = Task.WhenAll(task1, task2).Result;
    }
    ....
}

this can be made so much easier with this..

var httpClient = A.Fake<IHttpClient>();
A.CallTo(() = >httpClient.GetStringAsync("http://www.google.com.au"))
    .Returns("gooz was here");
A.CallTo(() = >httpClient.GetStringAsync("http://www.microsoft.com.au"))
    .Returns("ms was here");

clean clean clean :)

Then - there is the next bit : Discoverability

When I first started using MS.Net.Http.HttpClient the api was pretty obvious :+1: Ok - get string and do it asyncly.. simple ...

... but then I hit testing .... and now I'm suppose to learn about HttpClientHandlers? Um why? I feel that this should all be hidden under the covers and I shouldn't have to worry about all this implementation detail. It's too much! You're saying that I should start to look inside the box and learn some of the plumbing ... which hurts :cry:

It's all making this more complex than it should be, IMO.

Help me Microsoft - you're my only hope.

aarondandy commented 9 years ago

I too would love to see an easy and simple way to test various things that use HttpClient. :+1:

SidharthNabar commented 9 years ago

Thanks for the detailed response and code snippets - that really helps.

First, I notice that you are creating new instances of HttpClient for sending each request - that is not the intended design pattern for HttpClient. Creating one HttpClient instance and reusing it for all your requests helps optimize connection pooling and memory management. Please consider reusing a single instance of HttpClient. Once you do this, you can insert the fake handler into just one instance of HttpClient and you're done.

Second - you are right. The API design of HttpClient does not lend itself to a pure interface-based mechanism for testing. We are considering adding a static method/factory pattern in the future that will allow you to alter the behavior of all HttpClient instances created from that factory or after that method. We haven't yet settled on a design for this yet, though. But the key issue will remain - you will need to define a fake Handler and insert it below the HttpClient object.

@ericstj - thoughts on this?

Thanks, Sid.

PureKrome commented 9 years ago

@SidharthNabar why are you/team so hesitant to offering an interface for this class? I was hoping that the whole point of a developer having to learn about handlers and then having to create fake handler classes is enough of a pain point to justify (or at the very least, highlight) the need for an interface?

luisrudge commented 9 years ago

Yeah, I don't see why an Interface would be a bad thing. It would make testing HttpClient so much easier.

ericstj commented 9 years ago

One of the main reasons we avoid interfaces in the framework is that they don't version well. Folks can always create their own if they have a need and that won't get broken when the next version of the framework needs to add a new member. @KrzysztofCwalina or @terrajobst might be able to share more of the history/detail of this design guideline. This particular issue is a near religious debate: I'm too pragmatic to take part in that.

HttpClient has lots of options for unit testability. It's not sealed and most of its members are virtual. It has a base class that can be used to simplify the abstraction as well as a carefully designed extension point in HttpMessageHandler as @sharwell points out. IMO it's a quite well designed API, thanks to @HenrikFrystykNielsen.

TerribleDev commented 9 years ago

:+1: an interface

PureKrome commented 9 years ago

:wave: @ericstj Thanks heaps for jumping in :+1: :cake:

One of the main reasons we avoid interfaces in the framework is that they don't version well.

Yep - great point.

This particular issue is a near religious debate

yeah ... point well taken on that.

HttpClient has lots of options for unit testability

Oh? I'm struggling on this :blush: hence the reason for this issue :blush:

It's not sealed and most of its members are virtual.

Members are virtual? Oh drats, I totally missed that! If they are virtual, then mocking frameworks can mock those members out :+1: and we don't need an interface!

Lets have a looksies at HttpClient.cs ...

public Task<HttpResponseMessage> GetAsync(string requestUri) { .. }
public Task<HttpResponseMessage> GetAsync(Uri requestUri) { .. }

hmm. those aren't virtual ... lets search the file ... er .. no virtual keyword found. So do you mean other members to other related classes are virtual? If so .. then we're back at the issue I'm raising - we have to now look under the hood to see what GetAsync is doing so we then know what to create/wireup/etc...

I guess I'm just not understanding something really basic, here ¯(°_°)/¯ ?

EDIT: maybe those methods can be virtual? I can PR!

ericstj commented 9 years ago

SendAsync is virtual and almost every other API layers on top of that. 'Most' was incorrect, my memory serves me wrong there. My impression was that most were effectively virtual since they build on top of a virtual member. Usually we don't make things virtual if they are cascading overloads. There is a more specific overload of SendAsync that is not virtual, that one could be fixed.

PureKrome commented 9 years ago

Ah! Gotcha :blush: So all those methods end up calling SendAsync .. which does all the heavy lifting. So this still means we have a discoverability issue here .. but lets put that aside (that's opinionated).

How would we mock SendAsync give this basic sample... Install-Package Microsoft.Net.Http Install-Package xUnit

public class SomeService
{
    public async Task<string> GetSomeData(string url)
    {
        string result;
        using (var httpClient = new HttpClient())
        {
            result = await httpClient.GetStringAsync(url);
        }

        return result;
    }
}

public class Facts
{
    [Fact]
    public async Task GivenAValidUrl_GetSomeData_ReturnsSomeData()
    {
        // Arrange.
        var service = new SomeService();

        // Act.
        var result = await service.GetSomeData("http://www.google.com");

        // Assert.
        Assert.True(result.Length > 0);
    }
}
MrJul commented 9 years ago

One of the main reasons we avoid interfaces in the framework is that they don't version well. Folks can always create their own if they have a need and that won't get broken when the next version of the framework needs to add a new member.

I can totally understand this way of thinking with the full .NET Framework.

However, .NET Core is split into many small packages, each versioned independently. Use semantic versioning, bump the major version when there's a breaking change, and you're done. The only people affected will be the ones explicitly updating their packages to the new major version - knowing there are documented breaking changes.

I'm not advocating that you should break every interface every day: breaking changes should ideally be batched together into a new major version.

I find it sad to cripple APIs for future compatibility reasons. Wasn't one of the goal of .NET Core to iterate faster since you don't have to worry anymore about a subtle .NET update breaking thousands of already installed applications?

My two cents.

luisrudge commented 9 years ago

@MrJul :+1: Versioning shouldn't be a problem. That's why the version numbers exist :)

ericstj commented 9 years ago

Versioning is still very much an issue. Adding a member to an interface is a breaking change. For core libraries like this that are inbox in desktop we want to bring back features to desktop in future versions. If we fork it means folks can't write portable code that will run in both places. For more information on breaking changes have a look at: https://github.com/dotnet/corefx/wiki/Breaking-Changes.

I think this is a good discussion, but as I mentioned before I don't have a lot of scratch in the argument around interfaces vs abstract classes. Perhaps this is a topic to bring to the next design meeting?

I'm not super familiar with the test library you're using, but what I'd do is provide a hook to allow tests to set the instance of the client and then create a mock object that behaved however I want. The mock object could be mutable to permit some reuse.

If you have a specific compatible change you'd like to suggest to HttpClient that improves unit testability please propose it and @SidharthNabar and his team can consider it.

KrzysztofCwalina commented 9 years ago

If the API is not mockable, we should fix it. But it has nothing to do with interfaces vs classes. Interface is no different than pure abstract class from mockability perspective, for all practical purposes.

luisrudge commented 9 years ago

@KrzysztofCwalina you, sir, hit the nail on the head! perfect summary!

drub0y commented 9 years ago

I guess I'm going to take the less popular side of this argument as I personally do not think an interface is necessary. As has already been pointed out, HttpClient is the abstraction already. The behavior really comes from the HttpMessageHandler. Yes, it's not mockable/fakeable using the approaches we've become accustom to with frameworks like Moq or FakeItEasy, but it doesn't need to be; that's not the only way there is do things. The API is still perfectly testable by design though.

So let's address the "I need to create my own HttpMessageHandler?". No, of course not. We didn't all write our own mocking libraries. @PureKrome has already shown his HttpClient.Helpers library. Personally I have not used that one, but I will check it out. I've been using @richardszalay's MockHttp library which I find fantastic. If you've ever worked with AngularJS's $httpBackend, MockHttp is taking exactly the same approach.

Dependency wise, your service class should allow an HttpClient to be injected and obviously can supply the reasonable default of new HttpClient(). If you need to be able to create instances, take a Func<HttpClient> or, if you're not a fan of Func<> for whatever reason, design your own IHttpClientFactory abstraction and a default implementation of that would, again, just return new HttpClient().

In closing, I find this API perfectly testable in its current form and see no need for an interface. Yes, it requires a different approach, but the aforementioned libraries already exist to help us with this differnt style of testing in perfectly logical, intuitive ways. If we want more functionality/simplicity let's contribute to those libraries to make them better.

luisrudge commented 9 years ago

Personally, an Interface or virtual methods doesn't matter that much to me. But c'mon, perfectly testable is a little to much :)

drub0y commented 9 years ago

@luisrudge Well can you give a scenario that can't be tested using the message handler style of testing that something like MockHttp enables? Maybe that would help make the case.

I haven't come across anything I couldn't codify yet, but maybe I'm missing some more esoteric scenarios that can't be covered. Even then, might just be a missing feature of that library that someone could contribute.

For now I maintain the opinion that it's "perfectly testable" as a dependency, just not in a way .NET devs are used to.

luisrudge commented 9 years ago

It's testable, I agree. but it's still a pain. If you have to use an external lib that helps you do that, it's not perfectly testable. But I guess that's too subjective to have a discussion over it.

luisrudge commented 9 years ago

One could argue that aspnet mvc 5 is perfectly testable, you just have to write 50LOC to mock every single thing a controller needs. IMHO, that's the same case with HttpClient. It's testable? Yes. It's easy to test? No.

drub0y commented 9 years ago

@luisrudge Yes, I agree, it's subjective and I completely understand the desire for interface/virtuals. I'm just trying to make sure anyone who comes along and reads this thread will at least get some education on the fact that this API can be leveraged in a code base in a very testable way without introducing all of your own abstractions around HttpClient to get there.

if you have to use an external lib that helps you do that, it's not perfectly testable

Well, we're all using one library or another for mocking/faking already* and, it's true, we can't just use that one for testing this style of API, but I don't think that means its any less testable than an interface based approach just because I have to bring in another library.

* At least I hope not!

PureKrome commented 9 years ago

@drub0y from my OP I've stated that the library is testable - but it's just a real pain compared (to what i passionately believe) to what it could be. IMO @luisrudge spelt it out perfectly :

One could argue that aspnet mvc 5 is perfectly testable, you just have to write 50LOC to mock every single thing a controller needs

This repo is a major part of a large number of developers. So the default (and understandable) tactic is to be very cautious with this repo. Sure - I totally get that.

I'd like to believe that this repo is still in a position to be tweaked (with respect to the API) before it goes RTW. Future changes suddenly become really hard to do, include the perception to change.

So with the current api - can it be tested? Yep. Does it pass the Dark Matter Developer test? I personally don't think so.

The litmus test IMO is this: can a regular joe pickup one of the common/popular test frameworks + mocking frameworks and mock any of the main methods in this API? Right now - nope. So then the developer needs to stop what they're doing and start learning about the implimentation of this library.

As has already been pointed out, HttpClient is the abstraction already. The behavior really comes from the HttpMessageHandler.

This is my point - why are you making us having to spend cycles figuring this out when the library's purpose is to abstract all that magic .. only to say "Ok .. so we've done some magic but now that you want to mock our magic ... I'm sorry, you're now going to have to pull up your sleeves, lift up the roof of the library and start digging inside, etc".

It feels so ... convoluted.

We're in a position to make life easy for so many people and to keep coming back to the defensive position of : "It can be done - but .. read the FAQ/Code".

Here's another angle to approach this issue: Give 2 example to random Non-MS devs ... joe citizens developers that know how to use xUnit and Moq/FakeItEasy/InsertTheHipMockingFrameworkThisYear.

It distills down to that, IMO.

See which developer can solve that problem and stay happy.

If the API is not mockable, we should fix it.

Right now it's not IMO - but there's ways to get around this successfully (again, that's opinionated - I'll concede that)

But it has nothing to do with interfaces vs classes. Interface is no different than pure abstract class from mockability perspective, for all practical purposes.

Exactly - that's an implementation detail. Goal is to be able to use a battle-tested mock framework and off you go.

@luisrudge if you have to use an external lib that helps you do that, it's not perfectly testable @drub0y Well, we're all using one library or another for mocking/faking already

(I hope I understood that last quote/paragraph..) Not ... quiet. What @luisrudge was saying is: "We have one tool for testing. A second tool for mocking. So far, those a generic/overall tools not tied to anything. But .. you now want me to download a 3rd tool which is specific to mocking a specific API/service in our code because that specific API/service we're using, wasn't designed to be tested nicely?". That's a bit rich :(

Dependency wise, your service class should allow an HttpClient to be injected and obviously can supply the reasonable default of new HttpClient()

Completely agreed! So, can you refactor the sample code above to show us how easy this should/can be? There's many ways to skin a cat ... so what's a simple AND easy way? Show us the code.

I'll start. Appologies to @KrzysztofCwalina for using an interface, but this is just to kickstart my litmus test.

public interface IHttpClient
{
    Task<string> GetStringAsync(string url);
}

public class SomeService
{
    private IHttpClient _httpClient;

    // Injected.
    public SomeService(IHttpClient httpClient) { .. }

    public async Task<string> GetSomeData(string url)
    {
        string result;
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            result = await httpClient.GetStringAsync(url);
        }

        return result;
    }    
}
sharwell commented 9 years ago

It sounds like all @PureKrome needs is a documentation update explaining which methods to mock/override in order to customize the behavior of the API during testing.

luisrudge commented 9 years ago

@sharwell that's absolutely not the case. When I test stuff, I don't want to run the entire httpclient code: Look the SendAsync method. It's insane. What you're suggesting is that every developer should know the internals from the class, open github, see the code and that kind of stuff to be able to test it. I want that developer to mock the GetStringAsync and get shit done.

sharwell commented 9 years ago

@luisrudge Actually my suggestion would be to avoid mocking for this library altogether. Since you already have a clean abstraction layer with a decoupled, replaceable implementation, additional mocking is unnecessary. Mocking for this library has the following additional downsides:

  1. Increased burden on developers creating tests (maintaining the mocks), which in turn increases development costs
  2. Increased likelihood that your tests will fail to detect certain problematic behaviors, such as reusing the content stream associated with a message (sending a message results in a call to Dispose on the content stream, but most mocks will ignore this)
  3. Unnecessarily tighter coupling of the application to a particular abstraction layer, which increases development costs associated with evaluating alternatives to HttpClient

Mocking is a testing strategy targeting intertwined codebases that are difficult to test at a small scale. While the use of mocking correlates with increased development costs, the need for mocking correlates with increased amount of coupling in the code. This means mocking itself is a code smell. If you can provide the same input-output test coverage across and within your API without using mocking, you will benefit in basically every development aspect.

ctolkien commented 9 years ago

Since you already have a clean abstraction layer with a decoupled, replaceable implementation, additional mocking is unnecessary

With respect, if the option is having to write your own Faked Message Handler, which isn't obvious without digging through the code, then that is a very high barrier that is going to be hugely frustrating for a lot of developers.

I agree with @luisrudge 's comment earlier. Technically MVC5 was testable, but my god was it a pain in the ass and an immense source of hair pulling.

luisrudge commented 9 years ago

@sharwell Are you saying that mocking IHttpClient is a burden and implementing a fake message handler (like this one isn't? I can't agree with that, sorry.

sharwell commented 9 years ago

@luisrudge For simple cases of testing GetStringAsync, it's not nearly that hard to mock the handler. Using out-of-the-box Moq + HttpClient, all you need is this:

Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";

Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) }));
HttpClient httpClient = new HttpClient(mockHandler.Object);
string result = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false);
Assert.AreEqual(expectedResponse, result);

If that's too much, you can define a helper class:

internal static class MockHttpClientHandlerExtensions
{
    public static void SetupGetStringAsync(this Mock<HttpClientHandler> mockHandler, Uri requestUri, string response)
    {
        mockHandler.Protected()
            .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
            .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(response) }));
    }
}

Using the helper class all you need is this:

Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";

Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.SetupGetStringAsync(requestUri, expectedResponse);
HttpClient httpClient = new HttpClient(mockHandler.Object);
string result = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false);
Assert.AreEqual(expectedResponse, result);
PureKrome commented 9 years ago

So to compare the two versions:
Disclaimer: this is untested, browser code. Also, I haven't used Moq in ages.

Current

public class SomeService(HttpClientHandler httpClientHandler= null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null
                                  ? new HttpClient
                                  : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

// Unit test...
// Arrange.
Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) };
Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(responseMessage);
HttpClient httpClient = new HttpClient(mockHandler.Object);
var someService = new SomeService(mockHandler.Object); // Injected...

// Act.
string result = await someService.GetSomethingFromTheInternetAsync();

// Assert.
Assert.AreEqual(expectedResponse, result);
httpClient.Verify(x => x.GetSomethingFromTheInternetAsync(), Times.Once);

versus offering a way to mock the API method directly.

Another way

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

// Unit tests...
// Arrange.
var response = new Foo();
var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.GetStringAsync(It.IsAny<string>))
    .ReturnsAsync(response);
var service = new SomeService(httpClient.Object); // Injected.

// Act.
var result = await service.GetSomethingFromTheInternetAsync();

// Assert.
Assert.Equals("something", foo.Something);
httpClient.Verify(x => x.GetStringAsync(It.IsAny<string>()), Times.Once);

distilling that down, it mainly comes to this (excluding the slight differece in SomeService) ...

var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) };
Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(responseMessage);
HttpClient httpClient = new HttpClient(mockHandler.Object);

vs this ...

var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.GetStringAsync(It.IsAny<string>))
    .ReturnsAsync(response);

Does the code roughly look about right for both? (both have to be fairly accurate, before we can compare the two).

luisrudge commented 9 years ago

@sharwell so I have to run my code through all HttpClient.cs code in order to run my tests? How is that less coupled with HttpClient?

raymens commented 9 years ago

We heavily use HttpClient in some of our systems and I wouldn't call it a perfectly fine testable piece of tech. I really like the library but having some sort of Interface or any other improvement for testing would be a major improvement IMO.

Kudo's to @PureKrome for his library, but it would be better if it wasn't necessary :)

tucaz commented 9 years ago

I just read all comments and learned a lot, but I have one question: why do you need to mock the HttpClient in your tests?

In most common scenarios functionality provided by HttpClient will already be wrapped in some class such as HtmlDownloader.DownloadFile(). Why don't just mock this class instead of its plumbing?

Another possible scenario that would require such mocking is when you have to parse a downloaded string and this piece of code requires testing for multiple different outputs. Even in this case, this functionality (parsing) could be extracted away to a class/method and tested without any mocks.

I'm not saying that the testability of it cannot be improved, neither implying that it shouldn't be. I just want to understand to learn some new stuff.

PureKrome commented 9 years ago

@tucaz Lets imagine I have a website that returns some information about yourself. Your age, name, birthday, etc. You also happen to have stocks in some companies.

You've said you own 100 units of AAA stock and 200 units of BBB stock? So, when we display your account details we should display how much money your stocks are worth.

To do this, we need to grab the current stock prices from another system. So to do this, we need to retrieve the stock's with a service, right? Lets to this...

Disclaimer: browser code...

public class StockService : IStockService
{
    private readonly IHttpClient _httpClient;

    public StockService(IHttpClient httpClient)
    {
        _httpClient = httpClient; // Optional.
    }

    public async Task<IEnumerable<StockResults>> GetStocksAsync(IEnumerable<string> stockCodes)
    {
        var queryString = ConvertStockCodeListIntoQuerystring();
        string jsonResult;
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            jsonResult = await httpClient.GetStringAsync("http:\\www.stocksOnline.com\stockResults?" + queryString);
        }

        return JsonConvert.DeserializeObject<StockResult>(jsonResult);
    }
}

Ok, so here we have a simple service that says: "Go get me the stock prices for stocks AAA and BBB";

So, I need to test this out:

and now I can easily test for those scenario's. I notice that when the httpClient blows up (throws an exception) .. this service blows up.. so maybe I need to add some logging and return null? donno .. but at least I can think about the problem and then handle it.

Of course - i 100% don't want to really hit the real web service during my normal unit tests. By injecting the mock, I can use that. Otherwise I can create a new instance and use that.

So i'm trying to establish that my suggestion is a hellava lot easier (to read/maintain/etc) that the current status quo.

In most common scenarios functionality provided by HttpClient will already be wrapped in some class such as HtmlDownloader.DownloadFile().

Why should we wrap HttpClient? It's already an abstraction over all the http :sparkles: underneath. It's a great library! I personally don't see what that would achieve?

tucaz commented 9 years ago

@PureKrome your usage example is exactly what I mean by wrapping the functionality in a wrapping class.

I'm not sure I can understand what you are trying to test, but for me looks like you are testing the underlying webservice when what you really need to assert is that whoever is consuming it will be able to handle whatever comes back from the GetStockAsync method.

Please consider the following:

private IStockService stockService;

[Setup]
public void Setup()
{
    stockService = A.Fake<IStockService>();

    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker1" }))
        .Returns(new StockResults[] { new StockResults { Price = 100m, Ticker = "Ticker1", Status = "OK" }});
    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker2" }))
        .Returns(new StockResults[] { new StockResults { Price = 0m, Ticker = "Ticker2", Status = "NotFound" }});
    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker3" }))
        .Throws(() => new InvalidOperationException("Some weird message"));
}

[Test]
public async void Get_Total_Stock_Quotes()
{
    var stockService = A.Fake<IStockService>();

    var total = await stockService.GetStockAsync(new [] { "Ticker1" });

    Assert.IsNotNull(total);
    Assert.IsGreaterThan(0, total.Sum(x => x.Price);
}

[Test]
public async void Hints_When_Ticker_Not_Found()
{
    var stockService = A.Fake<IStockService>();

    var total = await stockService.GetStockAsync(new [] { "Ticker2" });

    Assert.IsNotNull(total);
    Assert.AnyIs(x => x.Status == "NotFound");
}

[Test]
public async void Throws_InvalidOperationException_When_Error()
{
    var stockService = A.Fake<IStockService>();

    Assert.Throws(() => await stockService.GetStockAsync(new [] { "Ticker3" }), typeof(InvalidOperationException));
}

I'm proposing that you increase your abstraction one level up and deal with the actual business logic since there isn't anything that needs to be tested regarding the behavior of the GetStockAsync method.

The only scenario that I can think of needing to mock the HttpClient is when you need to test some retry logic depending on the response received from the server or something like that. In this case, you will be building a more complicated piece of code that will probably be extracted into a HttpClientWithRetryLogic class and not spread in every method you code that uses the HttpClient class.

If this is case, you can use the message handler as suggested or built a class to wrap all this retry logic because HttpClient will be a really small part of the important thing that your class does.

MrJul commented 9 years ago

@tucaz Didn't you just test that your fake returns what you configured, effectively testing the mocking library? @PureKrome wants to test the concrete StockService implementation.

The newly introduced level of abstraction should be wrapping HttpClient calls to be able to properly test StockService. Chances are that wrapper will simply be delegating every call to HttpClient, and add unnecessary complexity to the application.

You can always add a new layer of abstraction to hide non-mockable types. What's criticized in this thread is that mocking HttpClient is possible, but should probably be easier / in line of what's usually done with other libraries.

tucaz commented 9 years ago

@MrJul you are correct. That's why I said that what's been tested by his example is the webservice implementation itself and why I proposed (without code examples. My bad) that he mocks the IStockService so he can assert that the code consuming it is able to handle whatever the GetStockAsync returns.

That's what I think should be tested in the first place. Not the mocking framework (like I did) or the webservice implementation (like @PureKrome wants to do).

At the end of the day I can understand that it would be nice to have an IHttpClient interface, but fail to see any utility in these kinds of scenarios since IMO what's need to be tested is how consumers of the service abstraction (and therefore the webservice) can handle it's return appropriately.

luisrudge commented 9 years ago

@tucaz nope. Your test is wrong. You're testing the mocking library. Using @PureKrome's example, one would want to test three things:

If you just mock IStockService in your Controller (for example), you aren't testing none of the three things mentioned above.

tucaz commented 9 years ago

@luisrudge agree with all your points. But... is the value of testing all these things you enumerate worth it? I mean, if I'm that committed in testing all those basic things that are covered by multiple frameworks (json.net, httpclient, etc) I think I can handle the way it is possible to do it today.

Still, if I want to make sure I can deserialize some weird response, can't I just make a test isolating those aspects of the code without the need to add HttpClient inside the test? Also, there is no way to prevent that the server will send the correct response you are expecting in your tests all the time. What if there is a response that is not covered? Your tests won't get it and you will have problems anyway.

My opinion is that such tests you are proposing have minimal net value and you should work on it only if you have the other 99% of your application covered. But at the end it is all a matter of personal preference and this is a subjective subject where different people see different value. And this, of course, don't mean that HttpClient shouldn't be more testable.

I think that the design of this specific piece of code took into account all these points and even if it is not the optimal outcome I don't think that the cost of changing it now would be lower than the possible benefits.

The objective I had in mind when I first spoke was to help @PureKrome try not to focus his efforts on what I perceive is a low value task. But again, it all comes down to personal opinion at some point.

luisrudge commented 9 years ago

Being sure you hit the correct url has low net value for you? What about being sure you do a PUT request to edit a resource instead of a post? The other 99% of my app can be covered, if this call's fails, then nothing will work.

drub0y commented 9 years ago

Soooo, I really think talking about HttpClient::GetStringAsync is a poor way to make a case for being able to mock HttpClient in the first place. If you're using the any of the HttpClient::Get[String|Stream|ByteArray]Async methods you've already lost because you can't do any proper error handling as everything is hidden behind an HttpRequestException that would be thrown and that doesn't even expose important details like the HTTP status code. So that means whatever abstraction you've built on top of the HttpClient will have no ability to react to and translate specific HTTP errors to domain specific exceptions and now you've got yourself a leaky abstraction... and one that gives your domain service's callers terrible exception information no less.

I think it's important to point this out because, in most cases, this means if you were using a mocking approach you'd be mocking the HttpClient methods that return HttpResponseMessage which already gets you to the same amount of work that is required to mock/fake HttpMessageHandler::SendAsync. In fact, it's more! You would potentially have to mock multiple methods (e.g. [Get|Put|Delete|Send]Async) at the HttpClient level instead of simply mocking SendAsync at the HttpMessageHandler level.

@luisrudge also mentioned deserializing JSON, so now we're talking about working with HttpContent which means you're working with one of the methods that return HttpResponseMessage for sure. Unless you're saying you're bypassing the entire media formatter architecture and deserializing strings from GetStringAsync into object instances manually with JsonSerializer::Deserialize or whatever. If that's the case then I don't think we can really have a conversation about testing the API any more because that would just be using the API itself completely wrong. :disappointed:

tucaz commented 9 years ago

@luisrudge it is important indeed, but you don't need to touch HttpClient to make sure it is right. Just make sure that the source where you read the URL from is returning things correctly and you are good to go. Again, not need to make a integration test with HttpClient in it.

These are important things, but once you get them right the chances of messing it up are really, really low unless you have a leaking pipe somewhere else.

luisrudge commented 9 years ago

@tucaz agree. Still: It's the most important part and I'd like to have it tested :)

luisrudge commented 9 years ago

@drub0y I'm amazed that using a public method is wrong :)

drub0y commented 9 years ago

@luisrudge Well, a different discussion I suppose, but I don't feel it's making the case for an interface that can be mocked by ignoring the inherent flaws in the use of these "convenience" methods in all but the most remedial of coding scenarios (e.g. utility apps and stage demos). If you choose to use them in production code, that's your choice, but you do so while hopefully acknowledging their shortcomings.

Anyway, let's get back to what @PureKrome wanted to see which is how one should design their classes to fit with the HttpClient API design such that they can easily test their code.

First, here's an example domain service that is going to be making HTTP calls to fulfill the needs of its domain:

public interface ISomeDomainService
{
    Task<string> GetSomeThingsValueAsync(string id);
}

public class SomeDomainService : ISomeDomainService
{
    private readonly HttpClient _httpClient;

    public SomeDomainService() : this(new HttpClient())
    {

    }

    public SomeDomainService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<string> GetSomeThingsValueAsync(string id)
    {
        return await _httpClient.GetStringAsync("/things/" + Uri.EscapeUriString(id));
    }
}

As you can see, it allows an HttpClient instance to be injected and also has a default constructor that instantiates the default HttpClient with no other special settings (obviously this default instance can be customized, but I'll leave that out for brevity).

Ok, so what's a test method look like for this thing using the MockHttp library then? Let's see:

[Fact]
public async Task GettingSomeThingsValueReturnsExpectedValue()
{
    // Arrange
    var mockHttpMessageHandler = new MockHttpMessageHandler();
    mockHttpMessageHandler.Expect("http://unittest/things/123")
        .Respond(new StringContent("expected value"));

    SomeDomainService sut = new SomeDomainService(new HttpClient(mockHttpMessageHandler)
    {
        BaseAddress = new Uri("http://unittest")
    });

    // Act
    var value = await sut.GetSomeThingsValueAsync("123");

    // Assert
    value.Should().Be("expected value");
}

Err, that's pretty simple and straight forward; reads clear as day IMNSHO. Also, note that I'm leveraging FluentAssertions which, again, is another library I think a lot of us use and another strike against the "I don't want to have to bring in another library" argument.

Now, let me also illustrate why you'd probably never really want to use GetStringAsync for any production code. Look back at the SomeDomainService::GetSomeThingsValueAsync method and assume it's calling a REST API that actually returns meaningful HTTP status codes. For example, maybe a "thing" with the id of "123" doesn't exist and so the API would return a 404 status. So I want to detect that and model it as the following domain specific exception:

// serialization support excluded for brevity
public class SomeThingDoesntExistException : Exception
{
    public SomeThingDoesntExistException(string id)
    {
        Id = id;
    }

    public string Id
    {
        get;
        private set;
    }
}

Ok, so let's rewrite the SomeDomainService::GetSomeThingsValueAsync method to do that now:

public async Task<string> GetSomeThingsValueAsync(string id)
{
    try
    {
        return await _httpClient.GetStringAsync("/things/" + Uri.EscapeUriString(id));
    }
    catch(HttpRequestException requestException)
    {
        // uh, oh... no way to tell if it doesn't exist (404) or server maybe just errored (500)
    }
}

So that's what I was talking about when I said those basic "convenience" methods like GetStringAsync are not really "good" for production level code. All you can get is HttpRequestException and from there you cannot tell what status code you got and therefore cannot possibly translate it into the correct exception within a domain. Instead, you'd need to using GetAsync and interpret the HttpResponseMessage like so:

public async Task<string> GetSomeThingsValueAsync(string id)
{
    HttpResponseMessage responseMessage = await _httpClient.GetAsync("/things/" + Uri.EscapeUriString(id));

    if(responseMessage.IsSuccessStatusCode)
    {
        return await responseMessage.Content.ReadAsStringAsync();
    }
    else
    {
        switch(responseMessage.StatusCode)
        {
            case HttpStatusCode.NotFound:
                throw new SomeThingDoesntExistException(id);

            // any other cases you want to might want to handle specifically for your domain

            default:
                // Unhandled cases can throw domain specific lower level communication exceptions
                throw new HttpCommunicationException(responseMessage.StatusCode, responseMessage.ReasonPhrase);
        }
    }
}

Ok, so now let's write a test to validate that I get my domain specific exception when I request a URL that doesn't exist:

[Fact]
public void GettingSomeThingsValueForIdThatDoesntExistThrowsExpectedException()
{
    // Arrange
    var mockHttpMessageHandler = new MockHttpMessageHandler();

    SomeDomainService sut = new SomeDomainService(new HttpClient(mockHttpMessageHandler)
    {
        BaseAddress = new Uri("http://unittest")
    });

    // Act
    Func<Task> action = async () => await sut.GetSomeThingsValueAsync("SomeIdThatDoesntExist");

    // Assert
    action.ShouldThrow<SomeThingDoesntExistException>();
}

Ermahgerd it's even less code!!$!% Why? 'Cause I didn't even have to set up an expectation with MockHttp and it automagically returned me a 404 for the requested URL as a default behavior.

Magic is Real

@PureKrome also mentioned a case where he wants to instantiate new HttpClient instances from within a service class. Like I said before, in that case you'd take a Func<HttpClient> as a dependency instead to abstract yourself from the actual creation or you could introduce your own factory interface if you're not a fan of Func for whatever reason.

PureKrome commented 9 years ago

I deleted my reply - i want to think about @drub0y 's reply and code a bit.

luisrudge commented 9 years ago

@drub0y you forgot to mention that someone will have to figure it out that one needs to implement a MockHttpMessageHandler in the first place. That's the whole point of this discussion. It isn't straight forward enough. With aspnet5, you can test everything just mocking public class from interfaces or base classes with virtual methods, but this specific API entry is different, less discoverable and less intuitive.

drub0y commented 9 years ago

@luisrudge Sure, I can agree with that, but how did we ever figure out that we needed a Moq or FakeItEasy? How did we figure out that we needed an N/XUnit, FluentAssertions, etc? I don't really see this as any different.

There are great (smart) people out there in the community who recognize the need for this stuff and actually take the time to start up libraries like these to solve these problems. (Kudos to all of them!) Then the solutions that are actually "good" get noticed by others who recognize the same needs and start grow organically through OSS and evangelization by the other leaders in the community. I think things are better than ever in this regard with the power of OSS finally being realized in the .NET community.

Now, that said, I do personally believe that Microsoft should have provided something like MockHttp along with the delivery of the System.Net.Http APIs right "out of the box". The AngularJS team provided $httpBackEnd along with $http because they recognized people would absolutely need this capability to be able to effectively test their apps. I think the fact that Microsoft didn't identity this same problem is why there's so much "confusion" around best practices with this API and I think why we're all here having this discussion about it now. :disappointed: That said, I'm glad @richardszalay and @PureKrome have stepped up to the plate to try and fill that gap with their libraries and now I think we should get behind them to help improve their libraries however we can to make all of our lives easier. :smile: