Класс модульного тестирования, который оборачивает HttpClient - PullRequest
0 голосов
/ 19 ноября 2018

Я нахожусь в процессе написания модульных тестов для нового проекта, который мы создали, и одна из проблем, с которыми я столкнулся, заключается в том, как правильно выполнить модульное тестирование того, что эффективно оборачивает HttpClient. В этом случае я написал класс RestfulService, который предоставляет базовые методы для вызова службы REST из C #.

Вот простой интерфейс, который реализует класс:

public interface IRestfulService
{
    Task<T> Get<T>(string url, IDictionary<string, string> parameters, IDictionary<string, string> headers = null);

    Task<T> Post<T>(string url, IDictionary<string, string> parameters, object bodyObject, IDictionary<string, string> headers = null);

    Task<string> Put(string url, IDictionary<string, string> parameters, object bodyObject, IDictionary<string, string> headers = null);

    Task<string> Delete(string url, object bodyObject, IDictionary<string, string> headers = null);

    Task<FileResponse?> Download(string url, IDictionary<string, string> urlParams = null, IDictionary<string, string> headers = null);
}

и вот урезанная версия реализации для примера:

public class RestfulService : IRestfulService
    {
        private HttpClient httpClient = null;
        private NetworkCredential credentials = null;
        /* boiler plate code for config and what have you */
        private string Host => "http://localhost";
        private NetworkCredential Credentials => new NetworkCredential("sampleUser", "samplePassword");
        private string AuthHeader
        {
            get
            {
                if (this.Credentials != null)
                {
                    return string.Format("Basic {0}", Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(this.Credentials.UserName + ":" + this.Credentials.Password)));
                }
                else
                {
                    return string.Empty;
                }
            }
        }

        private HttpClient Client => this.httpClient = this.httpClient ?? new HttpClient();
        public async Task<T> Get<T>(string url, IDictionary<string, string> parameters, IDictionary<string, string> headers = null)
        {
            var result = await this.DoRequest(url, HttpMethod.Get, parameters, null, headers);
            if (typeof (T) == typeof (string))
            {
                return (T)(object)result;
            }
            else
            {
                return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(result);
            }
        }

        private async Task<string> DoRequest(string url, HttpMethod method, IDictionary<string, string> urlParams = null, object bodyObject = null, IDictionary<string, string> headers = null)
        {
            string fullRequestUrl = string.Empty;
            HttpResponseMessage response = null;
            if (headers == null)
            {
                headers = new Dictionary<string, string>();
            }

            if (this.Credentials != null)
            {
                headers.Add("Authorization", this.AuthHeader);
            }

            headers.Add("Accept", "application/json");
            fullRequestUrl = string.Format("{0}{1}{2}", this.Host.ToString(), url, urlParams?.ToQueryString());
            using (var request = new HttpRequestMessage(method, fullRequestUrl))
            {
                request.AddHeaders(headers);
                if (bodyObject != null)
                {
                    request.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(bodyObject), System.Text.Encoding.UTF8, "application/json");
                }

                response = await this.Client.SendAsync(request).ConfigureAwait(false);
            }

            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
            if (!response.IsSuccessStatusCode)
            {
                var errDesc = response.ReasonPhrase;
                if (!string.IsNullOrEmpty(content))
                {
                    errDesc += " - " + content;
                }

                throw new HttpRequestException(string.Format("RestfulService: Error sending request to web service URL {0}. Reason: {1}", fullRequestUrl, errDesc));
            }

            return content;
        }
    }

Как вы можете видеть из реализации, это довольно тонкая оболочка, которая обрабатывает такие вещи, как добавление заголовков аутентификации (извлечено из конфигурации) и некоторые другие мелкие базовые вещи.

Мой вопрос: как я могу высмеять вызов Client.SendAsync, чтобы вернуть предопределенные ответы, чтобы убедиться, что десериализация происходит правильно и что добавлены заголовки аутентификации? Будет ли более разумным убрать добавление заголовков аутентификации из DoRequest и проверить реализацию DoRequest перед выполнением моего теста?

1 Ответ

0 голосов
/ 20 ноября 2018

Мне удалось выяснить это, используя метод доступа для HttpClient, а затем издеваясь над HttpMessageHandler. Вот код, который я использовал.

public interface IHttpClientAccessor
    {
        HttpClient HttpClient
        {
            get;
        }
    }

    public class HttpClientAccessor : IHttpClientAccessor
    {
        public HttpClientAccessor()
        {
            this.HttpClient = new HttpClient();
        }

        public HttpClient HttpClient
        {
            get;
        }
    }

    public interface IRestfulService
    {
        Task<T> Get<T>(string url, IDictionary<string, string> parameters, IDictionary<string, string> headers = null);
        Task<T> Post<T>(string url, IDictionary<string, string> parameters, object bodyObject, IDictionary<string, string> headers = null);
        Task<string> Put(string url, IDictionary<string, string> parameters, object bodyObject, IDictionary<string, string> headers = null);
        Task<string> Delete(string url, object bodyObject, IDictionary<string, string> headers = null);
        Task<FileResponse? > Download(string url, IDictionary<string, string> urlParams = null, IDictionary<string, string> headers = null);
    }

    public class RestfulService : IRestfulService
    {
        private HttpClient httpClient = null;
        private NetworkCredential credentials = null;
        private IHttpClientAccessor httpClientAccessor;
        public RestfulService(IConfigurationService configurationService, IHttpClientAccessor httpClientAccessor)
        {
            this.ConfigurationService = configurationService;
            this.httpClientAccessor = httpClientAccessor;
        }

        public string AuthHeader
        {
            get
            {
                if (this.Credentials != null)
                {
                    return string.Format("Basic {0}", Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(this.Credentials.UserName + ":" + this.Credentials.Password)));
                }
                else
                {
                    return string.Empty;
                }
            }
        }

        private IConfigurationService ConfigurationService
        {
            get;
        }

        private string Host => "http://locahost/";
        private NetworkCredential Credentials => this.credentials ?? new NetworkCredential("someUser", "somePassword");
        private HttpClient Client => this.httpClient = this.httpClient ?? this.httpClientAccessor.HttpClient;
        public async Task<T> Get<T>(string url, IDictionary<string, string> parameters, IDictionary<string, string> headers = null)
        {
            var result = await this.DoRequest(url, HttpMethod.Get, parameters, null, headers);
            if (typeof (T) == typeof (string))
            {
                return (T)(object)result;
            }
            else
            {
                return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(result);
            }
        }

        private async Task<string> DoRequest(string url, HttpMethod method, IDictionary<string, string> urlParams = null, object bodyObject = null, IDictionary<string, string> headers = null)
        {
            string fullRequestUrl = string.Empty;
            HttpResponseMessage response = null;
            if (headers == null)
            {
                headers = new Dictionary<string, string>();
            }

            if (this.Credentials != null)
            {
                headers.Add("Authorization", this.AuthHeader);
            }

            headers.Add("Accept", "application/json");
            fullRequestUrl = string.Format("{0}{1}{2}", this.Host.ToString(), url, urlParams?.ToQueryString());
            using (var request = new HttpRequestMessage(method, fullRequestUrl))
            {
                request.AddHeaders(headers);
                if (bodyObject != null)
                {
                    request.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(bodyObject), System.Text.Encoding.UTF8, "application/json");
                }

                response = await this.Client.SendAsync(request).ConfigureAwait(false);
            }

            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
            if (!response.IsSuccessStatusCode)
            {
                var errDesc = response.ReasonPhrase;
                if (!string.IsNullOrEmpty(content))
                {
                    errDesc += " - " + content;
                }

                throw new HttpRequestException(string.Format("RestfulService: Error sending request to web service URL {0}. Reason: {1}", fullRequestUrl, errDesc));
            }

            return content;
        }
    }

А вот реализация для тестовых случаев:

private RestfulService SetupRestfulService(HttpResponseMessage returns, string userName = "notARealUser", string password = "notARealPassword")
    {
        var mockHttpAccessor = new Mock<IHttpClientAccessor>();
        var mockHttpHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
        var testServiceEndpoints = Options.Create<Configuration.ServiceEndpoints>(new Configuration.ServiceEndpoints()
        {OneEndPoint = "http://localhost/test", AnotherEndPoint = "http://localhost/test"});
        var testAuth = Options.Create<AuthOptions>(new AuthOptions()
        {Password = password, Username = userName});
        mockHttpHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()).ReturnsAsync(returns).Verifiable();
        mockHttpAccessor.SetupGet(p => p.HttpClient).Returns(new HttpClient(mockHttpHandler.Object));
        return new RestfulService(new ConfigurationService(testServiceEndpoints, testAuth), mockHttpAccessor.Object);
    }

    [Fact]
    public void TestAuthorizationHeader()
    {
        // notARealUser : notARealPassword
        var expected = "Basic bm90QVJlYWxVc2VyOm5vdEFSZWFsUGFzc3dvcmQ=";
        var service = this.SetupRestfulService(new HttpResponseMessage{StatusCode = HttpStatusCode.OK, Content = new StringContent("AuthorizationTest")});
        Assert.Equal(expected, service.AuthHeader);
    }

   [Fact]
   public async Task TestGetPlainString()
   {
        var service = this.SetupRestfulService(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent("test") });
        var result = await service.Get<string>("test", null, null);
        Assert.Equal("test", result);
   }

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...