A Horse From Belgrade
A Horse From Belgrade

Reputation: 123

How to mock / unit test HTTP Client - restease

tl;dr: I'm having trouble mocking restease**

Also, I realize I may be totally on the wrong track, so any suggestions / nudges in the right direction would be of great help. I am quite new to this.

I'm making a small HTTP Client library, built around RestEase. RestEase is nice and easy to use, but I'm having trouble mocking the calls for the purpose of unit testing.

I want to use moq and NUnit, but I can't properly mock the RestClient. Example (shortened for brevity):

IBrandFolderApi - interface needed by restease to send calls

public interface IBrandFolderApi
{
    [Post("services/apilogin")]
    Task<LoginResponse> Login([Query] string username, [Query] string password);
}

BrandfolderClient.cs - the main class

public class BrandfolderClient : IBrandfolderClient
{
    private IBrandFolderApi _brandFolderApi { get; set; } 

    public BrandfolderClient(string url)
    {
        _brandFolderApi = RestClient.For<IBrandFolderApi >(url);
    }

    public async Task<string> Login(string username, string password)
    {
        LoginResponse loginResponse = await _brandFolderApi .Login(username, password);
        if (loginResponse.LoginSuccess)
        {
            ....
        }
        ....            
        return loginResponse.LoginSuccess.ToString();
    }
}

The unit tests

public class BrandFolderTests
{
    BrandfolderClient  _brandfolderClient 
    Mock<IBrandFolderApi> _mockBrandFolderApii;
    
    
    [SetUp]
    public void Setup()
    {
         //The test will fail here, as I'm passing a real URL and it will try and contact it.
        //If I try and send any string, I receive an Invalid URL Format exception.
         string url = "https://brandfolder.companyname.io";
        _brandfolderClient = new BrandfolderClient  (url);
        _mockBrandFolderApii= new Mock<IBrandFolderApi>();
    }

    ....
}

So, I don't know how to properly mock the Restclient so it doesn't send an actual request to an actual URL.

The test is failing at the constructor - if I send a valid URL string, then it will send a call to the actual URL. If I send any other string, I get an invalid URL format exception.

I believe I haven't properly implemented something around the rest client, but I'm not sure where. I'm very stuck on this, I've been googling and reading like crazy, but I'm missing something and I don't know what.

Upvotes: 3

Views: 2157

Answers (3)

Nkosi
Nkosi

Reputation: 246998

So, I don't know how to properly mock the Restclient so it doesn't send an actual request to an actual URL.

You actually should not have any need to mock RestClient.

Refactor your code to depend explicitly on the abstraction you control

public class BrandfolderClient : IBrandfolderClient {
    private readonly IBrandFolderApi brandFolderApi;

    public BrandfolderClient(IBrandFolderApi brandFolderApi) {
        this.brandFolderApi = brandFolderApi; //RestClient.For<IBrandFolderApi >(url);
    }

    public async Task<string> Login(string username, string password) {
        LoginResponse loginResponse = await brandFolderApi.Login(username, password);
        if (loginResponse.LoginSuccess) {
            //....
        }

        //....

        return loginResponse.LoginSuccess.ToString();
    }
}

removing the tight coupling to static 3rd party implementation concerns will allow your subject to be more explicit about what it actually needs to perform its function.

This will also make it easier for the subject to be tested in isolation.

For example:

public class BrandFolderTests { 
    BrandfolderClient subject;
    Mock<IBrandFolderApi> mockBrandFolderApi;

    [SetUp]
    public void Setup() {
        mockBrandFolderApi = new Mock<IBrandFolderApi>();
        subject = new BrandfolderClient(mockBrandFolderApi.Object);
    }

    //....

    [Test]
    public async Task LoginTest() {
        //Arrange
        LoginResponse loginResponse = new LoginResponse() {
            //...
        };
    
        mockBrandFolderApi
            .Setup(x => x.Login(It.IsAny<string>(), It.IsAny<string>()))
            .ReturnsAsync(loginResponse);

        //Act        
        string response = await subject.Login("username", "password");
    
        //Assert        
        mockBrandFolderApi.Verify(x => x.Login(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
    }
}

In production code, register and configure the IBrandFolderApi abstraction with the container, applying what ever 3rd party dependencies are required

Startup.ConfigureServices

//...

ApiOptions apiOptions = Configuration.GetSection("ApiSettings").Get<ApiOptions>();
services.AddSingleton(apiOptions);

services.AddScoped<IBrandFolderApi>(sp => {
    ApiOptions options = sp.GetService<ApiOptions>();
    string url = options.Url;
    return RestClient.For<IBrandFolderApi>(url);
});

Where ApiOptions is used to store settings

public class ApiOptions {
    public string Url {get; set;}
    //... any other API specific settings
}

that can be defined in appsetting.json

{
  ....

  "ApiSettings": {
    "Url": "https://brandfolder.companyname.io"
  }
}

so that they are not hard coded all over you code.

Upvotes: 4

Cowboy
Cowboy

Reputation: 1066

Not sure how you are using verify on _httpClient, its not a mock. but what you are looking for is https://github.com/canton7/RestEase#custom-httpclient. Most people pass in factory for this


//constructor
public httpClientConstructor(string url, IHttpHandlerFactory httpHandler)
{
   var httpClient = new HttpClient(httpHandler.GetHandler())
   {
       BaseAddress = new Uri(url),
   };       
   _exampleApi = RestClient.For<IExampleApi>(url);
}

public interface IHttpHandlerFactory<T>
{
   T GetHandler() where T: HttpMessageHandler
}

Thanks Ankit Vijay https://stackoverflow.com/a/68240316/5963888

public class FakeHttpMessageHandler : HttpMessageHandler
{
    private readonly bool _isSuccessResponse;

    public FakeHttpMessageHandler(bool isSuccessResponse = true)
    {
        _isSuccessResponse = isSuccessResponse;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(
            new HttpResponseMessage(_isSuccessResponse ? HttpStatusCode.OK : HttpStatusCode.InternalServerError));
    }
}

[SetUp]
public void Setup()
{
    var fakeHandler = new Mock<IHttpHandlerFactory>();
    fakeHandler.Setup(e => e.GetHandler() ).Returns( new FakeHttpHandler() );
    _httpClient = new HttpClient(fakeHandler.Object);
    _exampleApi = new Mock<IExampleApi>();
}

Upvotes: 0

Ankit Vijay
Ankit Vijay

Reputation: 4078

The HttpClient comes from System.Net.Http, which is not easy to mock.

You can, however, create a test HttpClient by passing a fake HttpMessageHandler. Here is an example:

public class FakeHttpMessageHandler : HttpMessageHandler
{
    private readonly bool _isSuccessResponse;

    public FakeHttpMessageHandler(bool isSuccessResponse = true)
    {
        _isSuccessResponse = isSuccessResponse;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(
            new HttpResponseMessage(_isSuccessResponse ? HttpStatusCode.OK : HttpStatusCode.InternalServerError));
    }
}

You can create create a test instance of HttpClient as shown below:

var httpClient = new HttpClient(new FakeHttpMessageHandler(true))
            { BaseAddress = new Uri("baseUrl") };

Upvotes: 2

Related Questions