包装HttpClient的单元测试类

时间:2018-11-19 20:43:15

标签: c# unit-testing xunit

我正在为我们创建的新项目编写单元测试,而我遇到的问题之一是如何正确地对有效包装HttpClient的内容进行单元测试。在这种情况下,我编写了一个RestfulService类,该类公开了用于从C#调用REST服务的基本方法。

这是该类实现的简单接口:

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;
        }
    }

从实现中可以看到,它是一个非常薄的包装器,可以处理诸如添加auth头(从config中拉出)之类的事情以及其他一些基本的事情。

我的问题:如何模拟对Client.SendAsync的调用以返回预定的响应,以验证反序列化是否正确进行并且已添加auth标头?在运行测试之前,将添加的auth标头移出DoRequest并嘲笑DoRequest的实现是否更有意义?

1 个答案:

答案 0 :(得分:1)

我能够使用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少了一点,但是它使我不必再充实整个适配器模式并顺着兔子的洞去了。