代码之家  ›  专栏  ›  技术社区  ›  Adam H

包装httpclient的单元测试类

  •  1
  • Adam H  · 技术社区  · 7 年前

    我正在为我们创建的新项目编写单元测试,我遇到的一个问题是如何正确地对有效包装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 并模仿 多拉斯特 在运行我的测试之前?

    1 回复  |  直到 7 年前
        1
  •  1
  •   Adam H    7 年前

    我可以使用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 连同凭证和返回一个我可以调用函数的对象。这有点不太理想,但它避免了我必须充实整个适配器模式,并进入那个兔子洞。