단위 테스트에서 HttpClient 모의
단위 테스트에 사용할 코드를 래핑하는 데 몇 가지 문제가 있습니다. 문제는 이것입니다. IHttpHandler 인터페이스가 있습니다.
public interface IHttpHandler
{
HttpClient client { get; }
}
그리고 그것을 사용하는 클래스, HttpHandler :
public class HttpHandler : IHttpHandler
{
public HttpClient client
{
get
{
return new HttpClient();
}
}
}
그런 다음 클라이언트 구현을 삽입하기 위해 simpleIOC를 사용하는 Connection 클래스 :
public class Connection
{
private IHttpHandler _httpClient;
public Connection(IHttpHandler httpClient)
{
_httpClient = httpClient;
}
}
그리고이 클래스가있는 단위 테스트 프로젝트가 있습니다.
private IHttpHandler _httpClient;
[TestMethod]
public void TestMockConnection()
{
var client = new Connection(_httpClient);
client.doSomething();
// Here I want to somehow create a mock instance of the http client
// Instead of the real one. How Should I approach this?
}
이제 분명히 내 백엔드에서 데이터 (JSON)를 검색하는 Connection 클래스에 메서드가 있습니다. 그러나 저는이 클래스에 대한 단위 테스트를 작성하고 싶습니다. 분명히 실제 백엔드에 대한 테스트를 작성하고 싶지 않습니다. 나는 큰 성공없이 이것에 대한 좋은 대답을 구글에 시도했다. 나는 Moq를 사용하여 이전에 조롱했지만 httpClient와 같은 것은 결코 사용하지 않았습니다. 이 문제에 어떻게 접근해야합니까?
미리 감사드립니다.
인터페이스는 구체적인 HttpClient
클래스를 노출하므로이 인터페이스 를 사용하는 모든 클래스가 여기에 연결됩니다. 즉, 모의 할 수 없습니다.
HttpClient
인터페이스에서 상속되지 않으므로 직접 작성해야합니다. 데코레이터와 같은 패턴을 제안합니다 .
public interface IHttpHandler
{
HttpResponseMessage Get(string url);
HttpResponseMessage Post(string url, HttpContent content);
Task<HttpResponseMessage> GetAsync(string url);
Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}
수업은 다음과 같습니다.
public class HttpClientHandler : IHttpHandler
{
private HttpClient _client = new HttpClient();
public HttpResponseMessage Get(string url)
{
return GetAsync(url).Result;
}
public HttpResponseMessage Post(string url, HttpContent content)
{
return PostAsync(url, content).Result;
}
public async Task<HttpResponseMessage> GetAsync(string url)
{
return await _client.GetAsync(url);
}
public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
{
return await _client.PostAsync(url, content);
}
}
이 모든 것의 요점은 HttpClientHandler
자체 를 생성하는 것 HttpClient
입니다. 물론 IHttpHandler
다른 방식으로 구현하는 여러 클래스를 생성 할 수 있습니다 .
이 접근 방식의 주요 문제는 다른 클래스의 메서드를 호출하는 클래스를 효과적으로 작성하고 있지만 상속 하는 클래스를 만들 수 있다는 것 입니다 HttpClient
( Nkosi의 예제 참조 , 저보다 훨씬 나은 접근 방식입니다). HttpClient
조롱 할 수있는 인터페이스가 있다면 인생은 훨씬 더 쉬울 것 입니다. 불행히도 그렇지 않습니다.
그러나이 예는 골든 티켓 이 아닙니다 . IHttpHandler
여전히 네임 스페이스에 HttpResponseMessage
속하는 에 의존 System.Net.Http
하므로, 이외의 다른 구현이 필요한 경우 HttpClient
응답을 HttpResponseMessage
객체 로 변환하기 위해 일종의 매핑을 수행해야 합니다. 물론 이것은 여러 구현을 사용해야하는 경우 에만 문제 가 IHttpHandler
되지만 그렇게하는 것처럼 보이지는 않으므로 세상의 끝은 아니지만 생각해 볼 사항입니다.
어쨌든, 추상화 된 IHttpHandler
구체적인 HttpClient
클래스 에 대해 걱정할 필요없이 간단히 조롱 할 수 있습니다.
비동기 메서드를 여전히 호출하지만 비동기 메서드 단위 테스트에 대해 걱정할 필요가 없으므로 비동기 메서드를 테스트하는 것이 좋습니다. 여기를 참조 하세요.
HttpClient의 확장 성은 HttpMessageHandler
생성자에 전달 된 것입니다. 그 의도는 플랫폼 별 구현을 허용하는 것이지만이를 모의 할 수도 있습니다. HttpClient에 대한 데코레이터 래퍼를 만들 필요가 없습니다.
Moq를 사용하는 것보다 DSL을 선호하는 경우 GitHub / Nuget에 라이브러리가있어 작업이 좀 더 쉬워집니다. https://github.com/richardszalay/mockhttp
var mockHttp = new MockHttpMessageHandler();
// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localost/api/user/*")
.Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON
// Inject the handler or client into your application code
var client = new HttpClient(mockHttp);
var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;
var json = await response.Content.ReadAsStringAsync();
// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}
가장 좋은 방법은 HttpClient를 래핑하는 대신 HttpMessageHandler를 모의하는 것입니다. 이 대답은 여전히 HttpClient를 주입하여 단일 항목이되거나 종속성 주입으로 관리 될 수 있다는 점에서 고유합니다.
"HttpClient는 한 번 인스턴스화되고 응용 프로그램 수명 내내 재사용되도록 고안되었습니다." ( 출처 ).
SendAsync가 보호되기 때문에 HttpMessageHandler를 조롱하는 것은 약간 까다로울 수 있습니다. 다음은 xunit과 Moq를 사용한 완전한 예입니다.
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;
// Use nuget to install xunit and Moq
namespace MockHttpClient {
class Program {
static void Main(string[] args) {
var analyzer = new SiteAnalyzer(Client);
var size = analyzer.GetContentSize("http://microsoft.com").Result;
Console.WriteLine($"Size: {size}");
}
private static readonly HttpClient Client = new HttpClient(); // Singleton
}
public class SiteAnalyzer {
public SiteAnalyzer(HttpClient httpClient) {
_httpClient = httpClient;
}
public async Task<int> GetContentSize(string uri)
{
var response = await _httpClient.GetAsync( uri );
var content = await response.Content.ReadAsStringAsync();
return content.Length;
}
private readonly HttpClient _httpClient;
}
public class SiteAnalyzerTests {
[Fact]
public async void GetContentSizeReturnsCorrectLength() {
// Arrange
const string testContent = "test content";
var mockMessageHandler = new Mock<HttpMessageHandler>();
mockMessageHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage {
StatusCode = HttpStatusCode.OK,
Content = new StringContent(testContent)
});
var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object));
// Act
var result = await underTest.GetContentSize("http://anyurl");
// Assert
Assert.Equal(testContent.Length, result);
}
}
}
이것은 일반적인 질문이며 HttpClient를 조롱하는 기능을 원하고 있었지만 마침내 HttpClient를 조롱해서는 안된다는 것을 깨닫게 된 것 같습니다. 그렇게하는 것이 논리적으로 보이지만, 오픈 소스 라이브러리에서 볼 수있는 것들에 세뇌 당했다고 생각합니다.
우리는 종종 "클라이언트"를 보게되는데, 코드에서 모방하여 격리 된 상태에서 테스트 할 수 있으므로 동일한 원칙을 HttpClient에 자동으로 적용하려고합니다. HttpClient는 실제로 많은 일을합니다. 당신은 그것을 HttpMessageHandler의 관리자로 생각할 수 있으므로 그것을 조롱하고 싶지 않으며, 그것이 여전히 인터페이스가없는 이유 입니다. 단위 테스트 또는 서비스 설계에 대해 정말로 관심이있는 부분은 응답을 반환하는 HttpMessageHandler이며이를 조롱 할 수 있습니다 .
또한 HttpClient를 더 큰 거래로 취급하기 시작해야한다는 점도 지적 할 가치가 있습니다. 예 : 새 HttpClients를 최소한으로 유지하십시오. 재사용하면 재사용이 가능하도록 설계되었으며 그렇게 할 경우 자원을 덜 사용합니다. 당신이 그것을 더 큰 거래처럼 다루기 시작하면, 그것을 조롱하고 싶어하는 것이 훨씬 더 잘못 느껴질 것이고 이제 메시지 핸들러는 클라이언트가 아닌 당신이 주입하는 것이 될 것입니다.
즉, 클라이언트 대신 핸들러에 대한 종속성을 설계하십시오. 더 좋은 점은 HttpClient를 사용하는 추상 "서비스"로, 핸들러를 주입하고 대신 주입 가능한 종속성으로 사용할 수 있습니다. 그런 다음 테스트에서 핸들러를 가짜로 만들어 테스트 설정에 대한 응답을 제어 할 수 있습니다.
HttpClient를 래핑하는 것은 엄청난 시간 낭비입니다.
업데이트 : Joshua Dooms의 예를 참조하십시오. 바로 제가 추천하는 것입니다.
또한 주석에서 언급했듯이을 추상화 하여 HttpClient
결합되지 않도록해야합니다. 나는 과거에 비슷한 일을했습니다. 나는 당신이하려는 일로 내가 한 일을 조정하려고 노력할 것입니다.
먼저 HttpClient
클래스를 살펴보고 필요한 기능을 결정했습니다.
여기에 가능성이 있습니다.
public interface IHttpClient {
System.Threading.Tasks.Task<T> DeleteAsync<T>(string uri) where T : class;
System.Threading.Tasks.Task<T> DeleteAsync<T>(Uri uri) where T : class;
System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
System.Threading.Tasks.Task<T> PostAsync<T>(string uri, object package);
System.Threading.Tasks.Task<T> PostAsync<T>(Uri uri, object package);
System.Threading.Tasks.Task<T> PutAsync<T>(string uri, object package);
System.Threading.Tasks.Task<T> PutAsync<T>(Uri uri, object package);
}
앞서 언급했듯이 이것은 특정 목적을위한 것입니다. 나는 모든 것을 다루는 것에 대한 대부분의 의존성을 완전히 추상화 HttpClient
하고 내가 원하는 것에 집중했습니다. 원하는 HttpClient
기능 만 제공 하려면 을 추상화하는 방법을 평가해야합니다 .
이제 테스트에 필요한 것만 조롱 할 수 있습니다.
나는 IHttpHandler
완전히 제거 하고 HttpClient
추상화를 사용하는 것이 좋습니다 IHttpClient
. 그러나 처리기 인터페이스의 본문을 추상화 된 클라이언트의 구성원으로 바꿀 수 있으므로 선택하지 않습니다.
IHttpClient
그런 다음 의 구현을 사용하여 실제 / 콘크리트 HttpClient
또는 해당 문제에 대한 다른 개체 를 래핑 / 적용 할 수 있으며, 이는 실제로 원하는 기능을 제공하는 서비스 인 HTTP 요청을 만드는 데 사용할 수 있습니다 HttpClient
. 추상화를 사용하는 것은 깔끔하고 (내 의견) SOLID 접근 방식이며 프레임 워크가 변경됨에 따라 다른 것을 위해 기본 클라이언트를 전환해야하는 경우 코드를보다 유지 관리 할 수 있습니다.
다음은 구현 방법에 대한 스 니펫입니다.
/// <summary>
/// HTTP Client adaptor wraps a <see cref="System.Net.Http.HttpClient"/>
/// that contains a reference to <see cref="ConfigurableMessageHandler"/>
/// </summary>
public sealed class HttpClientAdaptor : IHttpClient {
HttpClient httpClient;
public HttpClientAdaptor(IHttpClientFactory httpClientFactory) {
httpClient = httpClientFactory.CreateHttpClient(**Custom configurations**);
}
//...other code
/// <summary>
/// Send a GET request to the specified Uri as an asynchronous operation.
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="uri">The Uri the request is sent to</param>
/// <returns></returns>
public async System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class {
var result = default(T);
//Try to get content as T
try {
//send request and get the response
var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
//if there is content in response to deserialize
if (response.Content.Headers.ContentLength.GetValueOrDefault() > 0) {
//get the content
string responseBodyAsText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
//desrialize it
result = deserializeJsonToObject<T>(responseBodyAsText);
}
} catch (Exception ex) {
Log.Error(ex);
}
return result;
}
//...other code
}
위의 예에서 볼 수 있듯이 일반적으로 사용과 관련된 많은 무거운 작업 HttpClient
이 추상화 뒤에 숨겨져 있습니다.
그런 다음 연결 클래스를 추상화 된 클라이언트로 주입 할 수 있습니다.
public class Connection
{
private IHttpClient _httpClient;
public Connection(IHttpClient httpClient)
{
_httpClient = httpClient;
}
}
그러면 테스트에서 SUT에 필요한 것을 모의 할 수 있습니다.
private IHttpClient _httpClient;
[TestMethod]
public void TestMockConnection()
{
SomeModelObject model = new SomeModelObject();
var httpClientMock = new Mock<IHttpClient>();
httpClientMock.Setup(c => c.GetAsync<SomeModelObject>(It.IsAny<string>()))
.Returns(() => Task.FromResult(model));
_httpClient = httpClientMock.Object;
var client = new Connection(_httpClient);
// Assuming doSomething uses the client to make
// a request for a model of type SomeModelObject
client.doSomething();
}
다른 답변을 기반으로 외부 종속성이없는이 코드를 제안합니다.
[TestClass]
public class MyTestClass
{
[TestMethod]
public async Task MyTestMethod()
{
var httpClient = new HttpClient(new MockHttpMessageHandler());
var content = await httpClient.GetStringAsync("http://some.fake.url");
Assert.AreEqual("Content as string", content);
}
}
public class MockHttpMessageHandler : HttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("Content as string")
};
return await Task.FromResult(responseMessage);
}
}
문제는 당신이 그것을 조금 거꾸로 가지고 있다는 것입니다.
public class AuroraClient : IAuroraClient
{
private readonly HttpClient _client;
public AuroraClient() : this(new HttpClientHandler())
{
}
public AuroraClient(HttpMessageHandler messageHandler)
{
_client = new HttpClient(messageHandler);
}
}
위의 수업을 보면 이것이 원하는 것 같아요. Microsoft는 최적의 성능을 위해 클라이언트를 활성 상태로 유지할 것을 권장하므로 이러한 유형의 구조를 사용하면 그렇게 할 수 있습니다. 또한 HttpMessageHandler는 추상 클래스이므로 mockable입니다. 테스트 방법은 다음과 같습니다.
[TestMethod]
public void TestMethod1()
{
// Arrange
var mockMessageHandler = new Mock<HttpMessageHandler>();
// Set up your mock behavior here
var auroraClient = new AuroraClient(mockMessageHandler.Object);
// Act
// Assert
}
이를 통해 HttpClient의 동작을 조롱하면서 로직을 테스트 할 수 있습니다.
죄송합니다. 이것을 작성하고 직접 시도한 후 HttpMessageHandler에서 보호 된 메서드를 조롱 할 수 없다는 것을 깨달았습니다. 이어서 적절한 모의 삽입을 허용하기 위해 다음 코드를 추가했습니다.
public interface IMockHttpMessageHandler
{
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}
public class MockHttpMessageHandler : HttpMessageHandler
{
private readonly IMockHttpMessageHandler _realMockHandler;
public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler)
{
_realMockHandler = realMockHandler;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _realMockHandler.SendAsync(request, cancellationToken);
}
}
이것으로 작성된 테스트는 다음과 같습니다.
[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
// Arrange
var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
// Set up Mock behavior here.
var auroraClient = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
// Act
// Assert
}
내 동료 중 하나는의 가장 주목 HttpClient
방법 모두 호출 SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
가상 방법은 떨어져있는 후드 아래를 HttpMessageInvoker
:
지금까지 조롱하는 가장 쉬운 방법 HttpClient
은 특정 방법을 조롱하는 것입니다.
var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);
코드는 HttpClient
일반 클래스 메서드를 포함하여 대부분 (전부는 아님)을 호출 할 수 있습니다.
httpClient.SendAsync(req)
https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs 를 확인 하려면 여기를 확인 하십시오.
한 가지 대안은 요청 URL과 일치하는 패턴을 기반으로 미리 준비된 응답을 반환하는 스텁 HTTP 서버를 설정하는 것입니다. 즉, 모의가 아닌 실제 HTTP 요청을 테스트합니다. 역사적으로 이것은 상당한 개발 노력이 필요했고 단위 테스트를 위해 고려하기에는 너무 느리지 만 OSS 라이브러리 WireMock.net 은 사용하기 쉽고 많은 테스트로 실행할 수있을만큼 빠르므로 고려할 가치가 있습니다. 설정은 몇 줄의 코드입니다.
var server = FluentMockServer.Start();
server.Given(
Request.Create()
.WithPath("/some/thing").UsingGet()
)
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBody("{'attr':'value'}")
);
여기에서 테스트에서 wiremock 사용에 대한 자세한 내용과 지침을 찾을 수 있습니다 .
파티에 조금 늦게 참여하지만 다운 스트림 REST 종속성이있는 dotnet 코어 마이크로 서비스의 통합 테스트 에서 가능할 때마다 wiremocking ( https://github.com/WireMock-Net/WireMock.Net )을 사용하는 것을 좋아 합니다.
IHttpClientFactory를 확장하는 TestHttpClientFactory를 구현하여 메서드를 재정의 할 수 있습니다.
HttpClient CreateClient (문자열 이름)
따라서 앱 내에서 명명 된 클라이언트를 사용할 때 wiremock에 연결된 HttpClient를 반환하는 것을 제어 할 수 있습니다.
이 접근 방식의 좋은 점은 테스트중인 애플리케이션 내에서 아무것도 변경하지 않고 서비스에 대한 실제 REST 요청을 수행하고 실제 다운 스트림 요청이 반환해야하는 json (또는 기타)을 조롱하는 과정 통합 테스트를 활성화한다는 것입니다. 이것은 간결한 테스트로 이어지고 애플리케이션에서 가능한 한 조롱을 최소화합니다.
public class TestHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
var httpClient = new HttpClient
{
BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
// G.Config is our singleton config access, so the endpoint
// to the running wiremock is used in the test
};
return httpClient;
}
}
과
// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);
HttpMessageHandler를 모방하고 테스트 중에 사용할 HttpClient 개체를 반환 할 수있는 RichardSzalay MockHttp 라이브러리를 사용할 수 있습니다 .
PM> 설치 패키지 RichardSzalay.MockHttp
MockHttp는 유창한 구성 API를 제공하고 미리 준비된 응답을 제공하는 HttpClient를 구동하는 엔진 인 대체 HttpMessageHandler를 정의합니다. 호출자 (예 : 애플리케이션의 서비스 계층)는 그 존재를 계속 인식하지 못합니다.
var mockHttp = new MockHttpMessageHandler();
// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localhost/api/user/*")
.Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON
// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();
var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;
var json = await response.Content.ReadAsStringAsync();
// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}
나는 많은 답변에 확신이 없습니다.
먼저 HttpClient
. HttpClient
구현에서 직접 인스턴스화해서는 안됩니다 . 인스턴스를 제공 할 책임이있는 공장을 주입해야 HttpClient
합니다. 이렇게하면 나중에 해당 공장 HttpClient
에서 조롱 HttpClient
하고 원하는대로 반품 할 수 있습니다 (예 : 실제 공장이 아닌 모의 ).
따라서 다음과 같은 공장을 갖게됩니다.
public interface IHttpClientFactory
{
HttpClient Create();
}
그리고 구현 :
public class HttpClientFactory
: IHttpClientFactory
{
public HttpClient Create()
{
var httpClient = new HttpClient();
return httpClient;
}
}
물론이 구현을 IoC 컨테이너에 등록해야합니다. Autofac을 사용하는 경우 다음과 같습니다.
builder
.RegisterType<IHttpClientFactory>()
.As<HttpClientFactory>()
.SingleInstance();
이제 적절하고 테스트 가능한 구현을 갖게 될 것입니다. 귀하의 방법이 다음과 같다고 상상해보십시오.
public class MyHttpClient
: IMyHttpClient
{
private readonly IHttpClientFactory _httpClientFactory;
public SalesOrderHttpClient(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<string> PostAsync(Uri uri, string content)
{
using (var client = _httpClientFactory.Create())
{
var clientAddress = uri.GetLeftPart(UriPartial.Authority);
client.BaseAddress = new Uri(clientAddress);
var content = new StringContent(content, Encoding.UTF8, "application/json");
var uriAbsolutePath = uri.AbsolutePath;
var response = await client.PostAsync(uriAbsolutePath, content);
var responseJson = response.Content.ReadAsStringAsync().Result;
return responseJson;
}
}
}
이제 테스트 부분입니다. HttpClient
extends HttpMessageHandler
, 이는 추상적입니다. HttpMessageHandler
우리가 mock을 사용할 때 각 테스트에 대해 각 동작을 설정할 수 있도록 델리게이트를 받아들이는 "mock"을 만들어 봅시다 .
public class MockHttpMessageHandler
: HttpMessageHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;
public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
{
_sendAsyncFunc = sendAsyncFunc;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _sendAsyncFunc.Invoke(request, cancellationToken);
}
}
이제 Moq (및 FluentAssertions, 단위 테스트를 더 읽기 쉽게 만드는 라이브러리)의 도움으로 PostAsync 메서드를 사용하는 단위 테스트에 필요한 모든 것을 갖추고 있습니다. HttpClient
public static class PostAsyncTests
{
public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
: Given_WhenAsync_Then_Test
{
private SalesOrderHttpClient _sut;
private Uri _uri;
private string _content;
private string _expectedResult;
private string _result;
protected override void Given()
{
_uri = new Uri("http://test.com/api/resources");
_content = "{\"foo\": \"bar\"}";
_expectedResult = "{\"result\": \"ok\"}";
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var messageHandlerMock =
new MockHttpMessageHandler((request, cancellation) =>
{
var responseMessage =
new HttpResponseMessage(HttpStatusCode.Created)
{
Content = new StringContent("{\"result\": \"ok\"}")
};
var result = Task.FromResult(responseMessage);
return result;
});
var httpClient = new HttpClient(messageHandlerMock);
httpClientFactoryMock
.Setup(x => x.Create())
.Returns(httpClient);
var httpClientFactory = httpClientFactoryMock.Object;
_sut = new SalesOrderHttpClient(httpClientFactory);
}
protected override async Task WhenAsync()
{
_result = await _sut.PostAsync(_uri, _content);
}
[Fact]
public void Then_It_Should_Return_A_Valid_JsonMessage()
{
_result.Should().BeEquivalentTo(_expectedResult);
}
}
}
분명히이 테스트는 어리 석고 우리는 실제로 우리의 모의를 테스트하고 있습니다. 그러나 당신은 아이디어를 얻습니다. 다음과 같은 구현에 따라 의미있는 논리를 테스트해야합니다.
- 응답의 코드 상태가 201이 아닌 경우 예외가 발생해야합니까?
- 응답 텍스트를 구문 분석 할 수없는 경우 어떻게해야합니까?
- 기타
이 답변의 목적은 HttpClient를 사용하는 것을 테스트하는 것이 었으며 이것은 그렇게하는 좋은 깨끗한 방법입니다.
여기 저에게 잘 맞는 간단한 솔루션이 있습니다.
moq mocking 라이브러리 사용.
// ARRANGE
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock
.Protected()
// Setup the PROTECTED method to mock
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
// prepare the expected response of the mocked http call
.ReturnsAsync(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("[{'id':1,'value':'1'}]"),
})
.Verifiable();
// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
BaseAddress = new Uri("http://test.com/"),
};
var subjectUnderTest = new MyTestClass(httpClient);
// ACT
var result = await subjectUnderTest
.GetSomethingRemoteAsync('api/test/whatever');
// ASSERT
result.Should().NotBeNull(); // this is fluent assertions here...
result.Id.Should().Be(1);
// also check the 'http' call was like we expected it
var expectedUri = new Uri("http://test.com/api/test/whatever");
handlerMock.Protected().Verify(
"SendAsync",
Times.Exactly(1), // we expected a single external request
ItExpr.Is<HttpRequestMessage>(req =>
req.Method == HttpMethod.Get // we expected a GET request
&& req.RequestUri == expectedUri // to this uri
),
ItExpr.IsAny<CancellationToken>()
);
출처 : https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/
이것은 오래된 질문이지만 여기서 보지 못한 해결책으로 답변을 확장하고 싶은 충동을 느낍니다.
Microsoft 어셈블리 (System.Net.Http)를 가짜로 만든 다음 테스트 중에 ShinsContext를 사용할 수 있습니다.
- VS 2017에서 System.Net.Http 어셈블리를 마우스 오른쪽 단추로 클릭하고 "가짜 어셈블리 추가"를 선택합니다.
- ShimsContext.Create () 아래의 단위 테스트 메서드에 코드를 넣으십시오. 이렇게하면 HttpClient를 가짜로 만들려는 코드를 격리 할 수 있습니다.
구현 및 테스트에 따라 HttpClient에서 메서드를 호출하고 반환 된 값을 위조하려는 모든 원하는 동작을 구현하는 것이 좋습니다. ShimHttpClient.AllInstances를 사용하면 테스트 중에 생성 된 모든 인스턴스에서 구현이 위조됩니다. 예를 들어 GetAsync () 메서드를 위조하려면 다음을 수행합니다.
[TestMethod] public void FakeHttpClient() { using (ShimsContext.Create()) { System.Net.Http.Fakes.ShimHttpClient.AllInstances.GetAsyncString = (c, requestUri) => { //Return a service unavailable response var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable); var task = Task.FromResult(httpResponseMessage); return task; }; //your implementation will use the fake method(s) automatically var client = new Connection(_httpClient); client.doSomething(); } }
DI 환경 에서처럼 아주 간단한 일을했습니다.
public class HttpHelper : IHttpHelper
{
private ILogHelper _logHelper;
public HttpHelper(ILogHelper logHelper)
{
_logHelper = logHelper;
}
public virtual async Task<HttpResponseMessage> GetAsync(string uri, Dictionary<string, string> headers = null)
{
HttpResponseMessage response;
using (var client = new HttpClient())
{
if (headers != null)
{
foreach (var h in headers)
{
client.DefaultRequestHeaders.Add(h.Key, h.Value);
}
}
response = await client.GetAsync(uri);
}
return response;
}
public async Task<T> GetAsync<T>(string uri, Dictionary<string, string> headers = null)
{
...
rawResponse = await GetAsync(uri, headers);
...
}
}
모의는 다음과 같습니다.
[TestInitialize]
public void Initialize()
{
...
_httpHelper = new Mock<HttpHelper>(_logHelper.Object) { CallBase = true };
...
}
[TestMethod]
public async Task SuccessStatusCode_WithAuthHeader()
{
...
_httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns(
Task<HttpResponseMessage>.Factory.StartNew(() =>
{
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(_testData))
};
})
);
var result = await _httpHelper.Object.GetAsync<TestDTO>(...);
Assert.AreEqual(...);
}
필요한 것은 ctor HttpMessageHandler
에게 전달하는 클래스 의 테스트 버전입니다 HttpClient
. 요점은 테스트 HttpMessageHandler
클래스 HttpRequestHandler
에 호출자가 설정하고 HttpRequest
원하는 방식으로 간단히 처리 할 수 있는 델리게이트가 있다는 것 입니다.
public class FakeHttpMessageHandler : HttpMessageHandler
{
public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
(r, c) =>
new HttpResponseMessage
{
ReasonPhrase = r.RequestUri.AbsoluteUri,
StatusCode = HttpStatusCode.OK
};
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(HttpRequestHandler(request, cancellationToken));
}
}
이 클래스의 인스턴스를 사용하여 구체적인 HttpClient 인스턴스를 만들 수 있습니다. HttpRequestHandler 대리자를 통해 HttpClient에서 나가는 http 요청을 완전히 제어 할 수 있습니다.
PointZeroTwo의 답변 에서 영감을 얻은 다음은 NUnit 및 FakeItEasy를 사용하는 샘플 입니다.
SystemUnderTest
이 예제에서는 테스트하려는 클래스입니다. 샘플 콘텐츠는 제공되지 않았지만 이미 가지고 있다고 가정합니다!
[TestFixture]
public class HttpClientTests
{
private ISystemUnderTest _systemUnderTest;
private HttpMessageHandler _mockMessageHandler;
[SetUp]
public void Setup()
{
_mockMessageHandler = A.Fake<HttpMessageHandler>();
var httpClient = new HttpClient(_mockMessageHandler);
_systemUnderTest = new SystemUnderTest(httpClient);
}
[Test]
public void HttpError()
{
// Arrange
A.CallTo(_mockMessageHandler)
.Where(x => x.Method.Name == "SendAsync")
.WithReturnType<Task<HttpResponseMessage>>()
.Returns(Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.InternalServerError,
Content = new StringContent("abcd")
}));
// Act
var result = _systemUnderTest.DoSomething();
// Assert
// Assert.AreEqual(...);
}
}
참고 URL : https://stackoverflow.com/questions/36425008/mocking-httpclient-in-unit-tests
'programing tip' 카테고리의 다른 글
수평 LinearLayout에 (수직) 구분선을 추가하는 방법은 무엇입니까? (0) | 2020.09.15 |
---|---|
zsh에서 내 프롬프트의 색상을 어떻게 변경할 수 있습니까 (일반 텍스트와 다름)? (0) | 2020.09.15 |
browserify 오류 / usr / bin / env : 노드 : 해당 파일 또는 디렉토리가 없습니다. (0) | 2020.09.15 |
'python'은 내부 또는 외부 명령으로 인식되지 않습니다. (0) | 2020.09.15 |
이 난독 화 된 C 코드는 main ()없이 실행된다고 주장하지만 실제로 어떤 역할을합니까? (0) | 2020.09.15 |