Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 591
A Horse From Belgrade Avatar asked Oct 15 '25 15:10

A Horse From Belgrade


1 Answers

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.

like image 88
Nkosi Avatar answered Oct 18 '25 07:10

Nkosi



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!