我正在为我们创建的新项目编写单元测试,而我遇到的问题之一是如何正确地对有效包装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
的实现是否更有意义?
答案 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少了一点,但是它使我不必再充实整个适配器模式并顺着兔子的洞去了。