Рефакторинг кода для модульного тестирования HttpClient - PullRequest
0 голосов
/ 11 июня 2019

Я имею дело с фрагментом кода, который выглядит следующим образом:

public class Uploader : IUploader
{
    public Uploader()
    {
        // assign member variables to dependency injected interface implementations
    }

    public async Task<string> Upload(string url, string data)
    {
        HttpResponseMessage result;
        try
        {
            var handler = new HttpClientHandler();
            var client = new HttpClient(handler);

            result = await client.PostAsync(url, new FormUrlEncodedContent(data));

            if (result.StatusCode != HttpStatusCode.OK)
            {
                return "Some Error Message";
            }
            else
            {
                return null; // Success!
            }
        }
        catch (Exception ex)
        {
            // do some fancy stuff here
        }
    }
}

Я пытаюсь выполнить модульное тестирование функции Upload.В частности, мне нужно издеваться над HttpClient.Прочитав другие ответы здесь и эти две статьи, я знаю, что один из лучших способов решить эту проблему - вместо этого смоделировать HttpMessageHandler и передать его HttpClientи пусть он возвращает все, что я хочу.

Итак, я начал по этому пути, сначала передав в конструкторе HttpClient в качестве зависимости:

public class Uploader : IUploader
{
    private readonly HttpClient m_httpClient; // made this a member variable

    public Uploader(HttpClient httpClient) // dependency inject this
    {
        m_httpClient = httpClient;
    }

    public async Task<string> Upload(string url, string data)
    {
        HttpResponseMessage result;
        try
        {
            var handler = new HttpClientHandler();

            result = await m_httpClient.PostAsync(url, new FormUrlEncodedContent(data));

            if (result.StatusCode != HttpStatusCode.OK)
            {
                return "Some Error Message";
            }
            else
            {
                return null; // Success!
            }
        }
        catch (Exception ex)
        {
            // do some fancy stuff here
        }
    }
}

и добавив: services.AddSingleton<HttpClient>();к ConfigureServices методу Startup.cs.

Но теперь я сталкиваюсь с небольшой проблемой, когда исходный код специально создает HttpClientHandler для передачи. Как мне тогда выполнить рефакторинг, чтобы принять в насмешливый обработчик?

Ответы [ 2 ]

1 голос
/ 11 июня 2019

Я считаю, что самый простой способ - это продолжать использовать HttpClient, но передать имитацию HttpClientHandler, например https://github.com/richardszalay/mockhttp

Пример кода по ссылке выше:

var mockHttp = new MockHttpMessageHandler();

mockHttp.When("http://localhost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}");

// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();

var response = await client.GetAsync("http://localhost/api/user/1234");

var json = await response.Content.ReadAsStringAsync();

Console.Write(json); // {'name' : 'Test McGee'}

Платформа Dependency Injection, встроенная в .NET Core, игнорирует конструкторы internal, поэтому в этом сценарии она вызывает конструктор без параметров.

public sealed class Uploader : IUploader
{
    private readonly HttpClient m_httpClient;

    public Uploader() : this(new HttpClientHandler())
    {
    }

    internal Uploader(HttpClientHandler handler)
    {
        m_httpClient = new HttpClient(handler);
    }

    // regular methods
}

В своих модульных тестах вы можете использовать конструктор, принимающийHttpClientHandler:

[Fact]
public async Task ShouldDoSomethingAsync()
{
    var mockHttp = new MockHttpMessageHandler();

    mockHttp.When("http://myserver.com/upload")
        .Respond("application/json", "{'status' : 'Success'}");

    var uploader = new Uploader(mockHttp);

    var result = await uploader.UploadAsync();

    Assert.Equal("Success", result.Status);
}

Обычно я не большой поклонник наличия внутреннего конструктора для облегчения тестирования, однако я считаю это более очевидным и автономным, чем регистрация общего HttpClient.

HttpClientFactory может быть еще одним хорошим вариантом, но я не слишком много с этим играюсь, поэтому я просто дам информацию о том, что мне показалось полезным.

0 голосов
/ 11 июня 2019

Одним из способов будет абстрагирование вашей функциональности HTTP в службу, например HttpService, которая реализует интерфейс IHttpService:

IHttpService

public interface IHttpService
{
    Task<HttpResponseMessage> Post(Uri url, string payload, Dictionary<string, string> headers = null);
}

HttpService

public class HttpService : IHttpService
{
    private static HttpClient _httpClient;

    private const string MimeTypeApplicationJson = "application/json";

    public HttpService()
    {
        _httpClient = new HttpClient();
    }

    private static async Task<HttpResponseMessage> HttpSendAsync(HttpMethod method, Uri url, string payload,
        Dictionary<string, string> headers = null)
    {
        var request = new HttpRequestMessage(method, url);
        request.Headers.Add("Accept", MimeTypeApplicationJson);

        if (headers != null)
        {
            foreach (var header in headers)
            {
                request.Headers.Add(header.Key, header.Value);
            }
        }

        if (!string.IsNullOrWhiteSpace(payload))
            request.Content = new StringContent(payload, Encoding.UTF8, MimeTypeApplicationJson);

        return await _httpClient.SendAsync(request);
    }

    public async Task<HttpResponseMessage> Post(Uri url, string payload, Dictionary<string, string> headers = null)
    {
        return await HttpSendAsync(HttpMethod.Post, url, payload, headers);
    }
}

Добавьте к вашим услугам:

services.AddSingleton<IHttpService, HttpService>();

В вашем классе вы затем добавите IHttpService в качестве зависимости:

public class Uploader : IUploader
{
    private readonly IHttpService _httpService; // made this a member variable

    public Uploader(IHttpService httpService) // dependency inject this
    {
        _httpService = httpService;
    }

    public async Task<string> Upload(string url, string data)
    {
        HttpResponseMessage result;
        try
        {

            result = await _httpService.PostAsync(new Uri(url), data);

            if (result.StatusCode != HttpStatusCode.OK)
            {
                return "Some Error Message";
            }
            else
            {
                return null; // Success!
            }
        }
        catch (Exception ex)
        {
            // do some fancy stuff here
        }
    }
}

Затем вы можете использовать Moq , чтобы высмеивать HttpService в вашем модульном тесте:

[TestClass]
public class UploaderTests
{
    private Mock<IHttpService> _mockHttpService = new Mock<IHttpService>();

    [TestMethod]
    public async Task WhenStatusCodeIsNot200Ok_ThenErrorMessageReturned()
    {
        // arrange  
        var uploader = new Uploader(_mockHttpService.Object);
        var url = "someurl.co.uk";
        var data = "data";

        // need to setup your mock to return the response you want to test
        _mockHttpService
            .Setup(s => s.PostAsync(url, data))
            .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError));

        // act
        var result = await uploader.Upload(new Uri(url), data);

        // assert
        Assert.AreEqual("Some Error Message", result);      
    }

    [TestMethod]
    public async Task WhenStatusCodeIs200Ok_ThenNullReturned()
    {
        // arrange  
        var uploader = new Uploader(_mockHttpService.Object);
        var url = "someurl.co.uk";
        var data = "data";

        // need to setup your mock to return the response you want to test
        _mockHttpService
            .Setup(s => s.PostAsync(new Uri(url), data))
            .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

        // act
        var result = await uploader.Upload(url, data);

        // assert
        Assert.AreEqual(null, result);      
    }
}
...