Terug naar overzicht

Memoires of a http request - How I got unit tested

Recently, I was working on a project where I needed to retrieve the access token from HttpContext and use this token to perform an HTTP request to get additional user info from an OpenID Connect (OIDC) UserInfo endpoint.
All this logic was encapsulated in a single service class.
Unit testing this class was giving me all sorts of issues and headaches, so I decided to document my findings and share them with you.

MG 6224

Used frameworks & libraries

  • ASP.NET Core 2.2.0
  • xunit 2.4.1
  • Moq 4.10.1
  • FluentAssertions 5.6.0
  • IdentityModel 3.10.0

The initial setup

Initially, the UserInfoService class was implemented as follows:

Pretty straightforward, right?
First we get the access token from HttpContext using the GetTokenAsync extension method in the Microsoft.AspNetCore.Authentication namespace.
Once we have the token, we get the additional info from the OIDC UserInfo endpoint using an HttpClient.
The userinfo endpoint is actually hosted on an identity server project. I won’t go into specifics regarding this setup, since this is beyond the scope of this article. Just know that the GetUserInfoAsync method is an extension method provided by the IdentityModel package that retrieves user info claims from an OIDC userinfo endpoint.

We have a perfectly working class, so what’s wrong? This will become clear when we start writing some unit tests. Let’s get to it!
Our first test looks like this:

Looking good… We’re simply mocking the HttpContext using the DefaultHttpContext class from ASP.NET Core and set up the GetTokenAsync method to return a fake token.
This should give us a perfectly valid UserInfoResponse.
Ok, let’s run the test.

Exception 4

Boom, that just blew up in our face!

Coyote 2

What happened? Basically, Moq is telling us that it can’t mock extension methods. We’ll resolve this in the next section.

Wrap it up

No, we’re not wrapping up this post just yet. We are going to wrap the HttpContext in a wrapper class, enabling us to mock our method.

Naturally, we’ll need to change the UserInfoService as well. Here’s the resulting GetUserInfoAsync method.

Now we can update our test accordingly:

We removed the DefaultHttpContext and are now using our IHttpContextWrapper instead. Running this gives us the following result:

Unittest 3

The exception has been resolved, but the test still doesn’t pass.

Ezgif com video to gif

This actually makes sense, doesn’t it? We’re basically doing an actual call to an endpoint on http://fake.sweetmustard.be. This host doesn’t exist, thus the IsError property on our result object is true.
When we inspect the object while debugging, this is exactly what we see:

Table 2

At the moment, we’re not able to mock the call from HttpClient that’s being used in our UserInfoService. This is our next step.

[Fact]ory

In ASP.NET Core, there’s a useful interface at our disposal that can help us to extract the object initialization of an HttpClient out of the UserInfoService, the IHttpClientFactory.
Let’s go back to our UserInfoService and create the HttpClient using the interface.

You’ll notice that the interface is injected in the constructor using Dependency Injection. In order for this to work, you’ll have to register the IHttpClientFactory in the IoC container.
If you’re using the ASP.NET Core built-in service container, you can use the AddHttpClient extension method on the IServiceCollection. More information about this extension method can be found here.

Now we have injected the interface, we can use it to create the HttpClient using the CreateClient method and we can now also easily setup a mock for the factory in our test.

Running this test gives us the exact same result as before.

Unittest 4

Why? Well, because we’re still doing the same thing as before. Setting up the mock for the IHttpClientFactory was only the first step to mocking a call through an HttpClient.
You may have already noticed, but we’re still returning a new HttpClient from our mock which will still do an actual request to the unknown host.
On to the next section… Bear with me, we’re almost there :)

Panda 2

Mock the response not the call

Alright, the final step. We want to be able to create an HttpClient that doesn’t send requests across the wire. Why not just mock the HttpClient and setup the GetUserInfoAsync method like the following example?

Because the GetUserInfoAsync is also an extension method and we’ll run into the same issue as before. So we need our HttpClient to be able to return a fake response. To accomplish this, we’ll create a MessageHandler that we can set up to return any response to our liking. You can do this by creating a class that inherits from the abstract class DelegatingHandler and overriding the SendAsync method.

Special thanks to Anthony Giretti, his article has been a big help for the creation of this handler.
And again, let’s update our test.

Notice that we’re setting the content of the response to a JSON string. The GetUserInfoAsync extension method expects a JSON claims response and will parse this into the UserInfoResponse class. With this in mind, you could even perform an additional assertion.

The resulting claims are now validated as well.
Let’s give our test a spin!

Unittest Passes 1

Hooray! It passes!

Hooray 1

Wrapping up, for real this time

Ok, we’ve covered several topics in order to be able to create a unit test for our call to an OIDC UserInfo endpoint.
First we’ve set up a wrapper for our HttpContext, so we could mock the GetTokenAsync extension method.
After that, we’ve created the HttpClient using the IHttpClientFactory interface. This enabled us to extract the HttpClient object initialization out of our service class. That way we can set up a testable client in our unit test.
As a final step, a DelegatingHandler was added so that we can simulate a response for an HTTP call.
That’s it!

Thank you for reading this article. Hopefully it can be of use to you.
Feel free to share your thoughts and comments. Feedback can be sent to thomas.vervaeke@sweetmustard.be.

Be sure to check out the side notes and useful links below for extra reading material.

Side notes

Some of you may have already noticed, throughout this post we’ve been doing multiple assertions in our test. I know that this isn’t considered a best practice, but to keep this post as simple and short as possible, I decided to work with a single test method.

Be sure to use the CreateClient method with the string name parameter on the IHttpClientFactory interface. There’s also a parameterless extension method in the System.Net.Http namespace from the Microsoft.Extensions.Http assembly.
But again, you guessed it, Moq will be unable to setup this method since it’s an extension method.

It is actually possible to mock the HttpContext without creating a wrapper around it, as follows.

Thanks to my colleague Wouter Huysentruit for the code snippet.
However, after discussing this, we agreed that this isn’t the best approach. It requires a lot of extra setup and is more prone to revision when the inner workings of HttpContext are changed in future updates of ASP.NET Core.
Adding a wrapper is just easier and future proof.

Useful links
https://openid.net/connect/
http://docs.identityserver.io/en/latest/
http://anthonygiretti.com/2018/09/06/how-to-unit-test-a-class-that-consumes-an-httpclient-with-ihttpclientfactory-in-asp-net-core/
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-2.2

Deze website maakt gebruik van cookies om ervoor te zorgen dat u de beste surfervaring op onze website krijgt. Meer info