Chế nhạo HttpClient trong các bài kiểm tra đơn vị


110

Tôi gặp một số vấn đề khi cố gắng gói mã của mình để sử dụng trong các bài kiểm tra đơn vị. Vấn đề là đây. Tôi có giao diện IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

Và lớp sử dụng nó, HttpHandler:

public class HttpHandler : IHttpHandler
{
    public HttpClient client
    {
        get
        {
            return new HttpClient();
        }
    }
}

Và sau đó là lớp Kết nối, sử dụng simpleIOC để đưa vào triển khai máy khách:

public class Connection
{
    private IHttpHandler _httpClient;

    public Connection(IHttpHandler httpClient)
    {
        _httpClient = httpClient;
    }
}

Và sau đó tôi có một dự án thử nghiệm đơn vị có lớp này:

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?     

}

Bây giờ rõ ràng là tôi sẽ có các phương thức trong lớp Connection sẽ lấy dữ liệu (JSON) từ một back end của tôi. Tuy nhiên, tôi muốn viết các bài kiểm tra đơn vị cho lớp này và rõ ràng là tôi không muốn viết các bài kiểm tra dựa trên back end thực sự, thay vì một bài kiểm tra bị chế nhạo. Tôi đã cố gắng google một câu trả lời tốt cho điều này mà không thành công lớn. Tôi có thể và đã sử dụng Moq để chế nhạo trước đây, nhưng chưa bao giờ sử dụng thứ gì đó như httpClient. Tôi nên tiếp cận vấn đề này như thế nào?

Cảm ơn trước.


1
Để lộ một HttpClienttrong giao diện của bạn là vấn đề nằm ở đâu. Bạn đang buộc khách hàng của mình sử dụng HttpClientlớp cụ thể. Thay vào đó, bạn nên hiển thị một phần trừu tượng của HttpClient.
Mike Eason

Bạn có thể giải thích nó sâu hơn một chút? Tôi nên xây dựng phương thức khởi tạo các lớp kết nối như thế nào vì tôi không muốn bất kỳ sự phụ thuộc nào của HttpClient trong các lớp khác sử dụng lớp Kết nối. Ví dụ: tôi không muốn truyền HttpClient riêng trong phương thức khởi tạo của Connection vì điều đó sẽ làm cho mọi lớp khác sử dụng Connection phụ thuộc vào HttpClient?
tjugg

Không quan tâm, bạn đã google những gì? Rõ ràng mockhttp có thể sử dụng một số cải tiến SEO.
Richard Szalay

@Mike - như đã đề cập trong câu trả lời của tôi, thực sự không cần thiết phải tóm tắt HttpClient. Nó hoàn toàn có thể kiểm tra được. Tôi có nhiều dự án có bộ thử nghiệm không có phụ trợ bằng phương pháp này.
Richard Szalay

Câu trả lời:


37

Giao diện của bạn hiển thị HttpClientlớp cụ thể , do đó bất kỳ lớp nào sử dụng giao diện này đều bị ràng buộc với nó, điều này có nghĩa là nó không thể bị chế nhạo.

HttpClientkhông kế thừa từ bất kỳ giao diện nào nên bạn sẽ phải viết của riêng mình. Tôi đề xuất một mẫu giống như người trang trí :

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

Và lớp của bạn sẽ trông như thế này:

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

Điểm mấu chốt của tất cả những điều này là HttpClientHandlertạo ra lớp riêng của nó HttpClient, tất nhiên bạn có thể tạo nhiều lớp triển khai IHttpHandlertheo những cách khác nhau.

Vấn đề chính với cách tiếp cận này là bạn đang viết một lớp hiệu quả mà chỉ gọi các phương thức trong một lớp khác, tuy nhiên bạn có thể tạo một lớp kế thừa từ đó HttpClient(Xem ví dụ của Nkosi , đó là một cách tiếp cận tốt hơn nhiều so với của tôi). Cuộc sống sẽ dễ dàng hơn nhiều nếu HttpClientcó một giao diện mà bạn có thể bắt chước, tiếc là không.

Tuy nhiên, ví dụ này không phải là tấm vé vàng. IHttpHandlervẫn dựa vào HttpResponseMessage, thuộc về System.Net.Httpkhông gian tên, do đó nếu bạn thực sự cần các triển khai khác HttpClient, bạn sẽ phải thực hiện một số loại ánh xạ để chuyển đổi các phản hồi của chúng thành HttpResponseMessagecác đối tượng. Điều này tất nhiên chỉ là một vấn đề nếu bạn cần phải sử dụng nhiều hiện thực của IHttpHandlernhưng nó không giống như bạn làm như vậy nó không phải là ngày tận thế, nhưng đó là một cái gì đó để suy nghĩ về.

Dù sao, bạn có thể đơn giản giả lập IHttpHandlermà không cần phải lo lắng về HttpClientlớp cụ thể vì nó đã được trừu tượng hóa.

Tôi khuyên bạn nên thử nghiệm phi async phương pháp, vì đây vẫn gọi các phương pháp không đồng bộ nhưng không có những rắc rối của việc phải lo lắng về đơn vị thử nghiệm phương pháp không đồng bộ, xem đây


Điều này thực sự trả lời câu hỏi của tôi. Câu trả lời Nkosis cũng đúng nên tôi không chắc mình nên chấp nhận câu trả lời nào, nhưng tôi sẽ đi với câu này. Cảm ơn bạn cho cả nỗ lực
tjugg

@tjugg Rất vui được giúp đỡ. Hãy bình chọn câu trả lời nếu bạn thấy chúng hữu ích.
Nkosi

3
Đáng chú ý là sự khác biệt chính giữa câu trả lời này và câu trả lời của Nkosi là đây là một phần trừu tượng mỏng hơn nhiều. Mỏng có lẽ là tốt cho một đối tượng khiêm tốn
Bến Aaronson

227

Khả năng mở rộng của HttpClient nằm ở chỗ HttpMessageHandlerđược truyền cho hàm tạo. Mục đích của nó là cho phép triển khai nền tảng cụ thể, nhưng bạn cũng có thể chế nhạo nó. Không cần tạo trình bao bọc trang trí cho HttpClient.

Nếu bạn muốn DSL sử dụng Moq, tôi có một thư viện trên GitHub / Nuget giúp mọi thứ dễ dàng hơn một chút: 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'}

1
Vì vậy, tôi sẽ chỉ chuyển MockHttpMessageHandler dưới dạng lớp Httphandler của trình xử lý tin nhắn? Hoặc làm thế nào bạn đã thực hiện nó trong các dự án của riêng bạn
tjugg

2
Câu trả lời tuyệt vời và điều mà tôi sẽ không biết ban đầu. Làm cho việc làm việc với HttpClient không quá tệ.
Bealer 21/02/17

6
Đối với những người không muốn xử lý việc tiêm cho khách hàng nhưng vẫn muốn khả năng kiểm tra dễ dàng, thì việc đạt được là rất nhỏ. Chỉ cần thay thế var client = new HttpClient()bằng var client = ClientFactory()và thiết lập một trường internal static Func<HttpClient> ClientFactory = () => new HttpClient();và ở cấp độ thử nghiệm, bạn có thể viết lại trường này.
Chris Marisic

3
@ChrisMarisic bạn đang đề xuất một hình thức dịch vụ vị trí để thay thế tiêm. Vị trí dịch vụ là một địa điểm chống nổi tiếng nên tiêm imho được ưa chuộng hơn.
MarioDS

2
@MarioDS và không phân biệt, bạn không nên tiêm một HttpClient dụ ở tất cả. Nếu bạn đã chết khi sử dụng constructer injection cho việc này thì bạn nên tiêm một HttpClientFactoryas in Func<HttpClient>. Vì tôi xem HttpClient hoàn toàn là một chi tiết triển khai và không phải là một phần phụ thuộc, tôi sẽ sử dụng tĩnh giống như tôi đã minh họa ở trên. Tôi hoàn toàn ổn với các bài kiểm tra thao tác bên trong. Nếu tôi quan tâm đến chủ nghĩa thuần túy, tôi sẽ thiết lập các máy chủ đầy đủ và thử nghiệm các đường dẫn mã trực tiếp. Sử dụng bất kỳ loại mô phỏng nào có nghĩa là bạn chấp nhận hành vi gần đúng không phải là hành vi thực tế.
Chris Marisic

39

Tôi đồng ý với một số câu trả lời khác rằng cách tiếp cận tốt nhất là chế nhạo HttpMessageHandler hơn là bọc HttpClient. Câu trả lời này là duy nhất ở chỗ nó vẫn tiêm HttpClient, cho phép nó là một singleton hoặc được quản lý bằng cách tiêm phụ thuộc.

"HttpClient được thiết kế để được khởi tạo một lần và được sử dụng lại trong suốt vòng đời của ứng dụng." ( Nguồn ).

Chế giễu HttpMessageHandler có thể hơi phức tạp vì SendAsync được bảo vệ. Đây là một ví dụ hoàn chỉnh, sử dụng xunit và 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);
        }
    }
}

1
Tôi thực sự thích điều này. Là mockMessageHandler.Protected()kẻ giết người. Cảm ơn vì ví dụ này. Nó cho phép viết bài kiểm tra mà không cần sửa đổi nguồn nào cả.
tyrion

1
FYI, Moq 4.8 hỗ trợ mạnh mẽ việc chế nhạo các thành viên được bảo vệ - github.com/Moq/moq4/wiki/Quickstart
Richard Szalay

2
Điều này trông tuyệt vời. Ngoài ra, Moq hỗ trợ ReturnsAsync nên mã sẽ trông như thế nào.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord

Cảm ơn @kord, tôi đã thêm điều đó vào câu trả lời
PointZeroTwo

3
Có cách nào để xác minh rằng "SandAsync" đã được gọi với một số tham số không? Tôi đã cố gắng sử dụng ... Protected (). Verify (...), nhưng có vẻ như nó không hoạt động với các phương thức không đồng bộ.
Rroman

29

Đây là một câu hỏi phổ biến và tôi rất muốn có khả năng chế giễu HttpClient, nhưng tôi nghĩ rằng cuối cùng tôi đã nhận ra rằng bạn không nên chế giễu HttpClient. Có vẻ hợp lý khi làm như vậy, nhưng tôi nghĩ chúng ta đã bị tẩy não bởi những thứ chúng ta thấy trong các thư viện mã nguồn mở.

Chúng tôi thường thấy "Khách hàng" ngoài đó mà chúng tôi mô phỏng trong mã của mình để chúng tôi có thể kiểm tra một cách riêng lẻ, vì vậy chúng tôi tự động cố gắng áp dụng nguyên tắc tương tự cho HttpClient. HttpClient thực sự làm được nhiều điều; bạn có thể coi nó như một trình quản lý cho HttpMessageHandler, vì vậy bạn không muốn chế nhạo điều đó và đó là lý do tại sao nó vẫn không có giao diện. Phần mà bạn thực sự quan tâm để kiểm tra đơn vị hoặc thiết kế dịch vụ của bạn, thậm chí là HttpMessageHandler vì đó là thứ trả về phản hồi và bạn có thể chế nhạo điều đó.

Cũng cần chỉ ra rằng bạn có thể nên bắt đầu coi HttpClient như một thỏa thuận lớn hơn. Ví dụ: Giữ cho việc cài đặt HttpClients mới của bạn ở mức tối thiểu. Tái sử dụng chúng, chúng được thiết kế để tái sử dụng và sử dụng ít tài nguyên hơn nếu bạn làm vậy. Nếu bạn bắt đầu coi nó như một thỏa thuận lớn hơn, bạn sẽ cảm thấy sai lầm hơn nhiều khi muốn chế nhạo nó và bây giờ trình xử lý tin nhắn sẽ bắt đầu là thứ mà bạn đang tiêm, không phải khách hàng.

Nói cách khác, hãy thiết kế các phần phụ thuộc của bạn xung quanh trình xử lý thay vì ứng dụng khách. Thậm chí tốt hơn, các "dịch vụ" trừu tượng sử dụng HttpClient cho phép bạn chèn một trình xử lý và thay vào đó sử dụng nó làm phần phụ thuộc có thể tiêm của bạn. Sau đó, trong các thử nghiệm của mình, bạn có thể giả mạo trình xử lý để kiểm soát phản hồi thiết lập các thử nghiệm của mình.

Bao bọc HttpClient là một sự lãng phí thời gian điên cuồng.

Cập nhật: Xem ví dụ của Joshua Dooms. Đó chính xác là những gì tôi đang giới thiệu.


17

Cũng như đề cập trong các ý kiến bạn cần phải trừu tượng đi HttpClientnhư vậy là không được kết hợp với nó. Tôi đã làm điều gì đó tương tự trong quá khứ. Tôi sẽ cố gắng điều chỉnh những gì tôi đã làm với những gì bạn đang cố gắng làm.

Đầu tiên, hãy xem HttpClientlớp và quyết định xem nó cung cấp chức năng nào cần thiết.

Đây là một khả năng:

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

Một lần nữa như đã nêu trước đây là cho các mục đích cụ thể. Tôi hoàn toàn loại bỏ hầu hết các phụ thuộc vào bất cứ thứ gì đang giải quyết HttpClientvà tập trung vào những gì tôi muốn trả về. Bạn nên đánh giá cách bạn muốn tóm tắt HttpClientđể chỉ cung cấp chức năng cần thiết mà bạn muốn.

Điều này bây giờ sẽ cho phép bạn chỉ bắt chước những gì cần thiết để kiểm tra.

Tôi thậm chí muốn khuyên bạn nên loại bỏ IHttpHandlerhoàn toàn và sử dụng sự HttpClienttrừu tượng IHttpClient. Nhưng tôi không chọn vì bạn có thể thay thế phần thân của giao diện trình xử lý của mình bằng các thành viên của ứng dụng khách trừu tượng.

Việc triển khai IHttpClientsau đó có thể được sử dụng để bao bọc / điều chỉnh một HttpClientđối tượng thực / cụ thể hoặc bất kỳ đối tượng nào khác cho vấn đề đó, có thể được sử dụng để thực hiện các yêu cầu HTTP như những gì bạn thực sự muốn là một dịch vụ cung cấp chức năng đó như được áp dụng HttpClientcụ thể. Sử dụng phần trừu tượng là một phương pháp tiếp cận SOLID rõ ràng (Ý kiến ​​của tôi) và có thể làm cho mã của bạn dễ bảo trì hơn nếu bạn cần chuyển ứng dụng khách cơ bản sang một thứ khác khi khung công tác thay đổi.

Đây là một đoạn mã về cách triển khai có thể được thực hiện.

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

Như bạn có thể thấy trong ví dụ trên, rất nhiều việc nặng nhọc thường liên quan đến việc sử dụng HttpClientđược ẩn sau sự trừu tượng.

Sau đó, lớp kết nối của bạn có thể được đưa vào với ứng dụng khách trừu tượng

public class Connection
{
    private IHttpClient _httpClient;

    public Connection(IHttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

Sau đó, bài kiểm tra của bạn có thể mô phỏng những gì cần thiết cho SUT của bạn

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

Đây là câu trả lời. Một phần trừu tượng ở trên HttpClientvà một bộ điều hợp để tạo phiên bản cụ thể của bạn bằng cách sử dụng HttpClientFactory. Làm điều này làm cho việc kiểm tra logic ngoài yêu cầu HTTP trở nên tầm thường, đó là mục tiêu ở đây.
pimbrouwers 18/09/18

13

Dựa trên các câu trả lời khác, tôi đề xuất mã này, không có bất kỳ phụ thuộc bên ngoài nào:

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

4
Bạn đang thử nghiệm hiệu quả mô hình của mình. Sức mạnh thực sự của mô hình giả là bạn có thể đặt kỳ vọng và thay đổi hành vi của nó trong mỗi bài kiểm tra. Thực tế là bạn phải HttpMessageHandlertự mình thực hiện một số điều khiến điều đó gần như không thể - và bạn phải làm như vậy bởi vì các phương pháp là như vậy protected internal.
MarioDS

3
@MarioDS Tôi nghĩ vấn đề là bạn có thể mô phỏng phản hồi HTTP để bạn có thể kiểm tra phần còn lại của mã. Nếu bạn đưa vào một nhà máy có HttpClient, thì trong các thử nghiệm, bạn có thể cung cấp HttpClient này.
chris31389

13

Tôi nghĩ vấn đề là bạn đã có một chút lộn ngược.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

    public AuroraClient() : this(new HttpClientHandler())
    {
    }

    public AuroraClient(HttpMessageHandler messageHandler)
    {
        _client = new HttpClient(messageHandler);
    }
}

Nếu bạn nhìn vào lớp học ở trên, tôi nghĩ đây là những gì bạn muốn. Microsoft khuyên bạn nên giữ máy khách tồn tại để có hiệu suất tối ưu, vì vậy kiểu cấu trúc này cho phép bạn làm điều đó. Ngoài ra, HttpMessageHandler là một lớp trừu tượng và do đó có thể giả lập. Phương pháp kiểm tra của bạn sau đó sẽ trông như thế này:

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var mockMessageHandler = new Mock<HttpMessageHandler>();
    // Set up your mock behavior here
    var auroraClient = new AuroraClient(mockMessageHandler.Object);
    // Act
    // Assert
}

Điều này cho phép bạn kiểm tra logic của mình trong khi chế nhạo hành vi của HttpClient.

Xin lỗi các bạn, sau khi viết bài này và tự mình thử, tôi nhận ra rằng bạn không thể chế nhạo các phương thức được bảo vệ trên HttpMessageHandler. Sau đó, tôi đã thêm mã sau để cho phép đưa vào một mô hình thích hợp.

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

Các bài kiểm tra được viết với điều này sau đó trông giống như sau:

[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
    // Arrange
    var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
    // Set up Mock behavior here.
    var client = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
    // Act
    // Assert
}

9

Một trong những đồng nghiệp của tôi nhận thấy rằng hầu hết các HttpClientphương thức đều gọi ẩn SendAsync(HttpRequestMessage request, CancellationToken cancellationToken), đó là một phương thức ảo trừ HttpMessageInvoker:

Cho đến nay, cách dễ nhất để chế nhạo HttpClientlà chỉ đơn giản là chế nhạo phương pháp cụ thể đó:

var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);

và mã của bạn có thể gọi hầu hết (nhưng không phải tất cả) các HttpClientphương thức lớp, bao gồm

httpClient.SendAsync(req)

Kiểm tra tại đây để xác nhận https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs


1
Điều này không hoạt động đối với bất kỳ mã nào gọi SendAsync(HttpRequestMessage)trực tiếp. Nếu bạn có thể sửa đổi mã của mình để không sử dụng chức năng tiện lợi này, thì việc chế nhạo HttpClient trực tiếp bằng cách ghi đè SendAsyncthực sự là giải pháp sạch nhất mà tôi đã tìm thấy.
Dylan Nicholson

8

Một giải pháp thay thế là thiết lập một máy chủ HTTP sơ khai trả về các phản hồi soạn trước dựa trên mẫu phù hợp với url yêu cầu, nghĩa là bạn kiểm tra các yêu cầu HTTP thực chứ không phải mô phỏng. Về mặt lịch sử, điều này sẽ cần nỗ lực phát triển đáng kể và sẽ còn lâu mới được xem xét để thử nghiệm đơn vị, tuy nhiên thư viện OSS WireMock.net rất dễ sử dụng và đủ nhanh để chạy với nhiều thử nghiệm nên có thể đáng xem xét. Thiết lập là một vài dòng mã:

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'}")
   );

Bạn có thể tìm thêm thông tin chi tiết và hướng dẫn về cách sử dụng wiremock trong các thử nghiệm tại đây.


8

Đây là một giải pháp đơn giản, phù hợp với tôi.

Sử dụng thư viện chế nhạo moq.

// 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>()
);

Nguồn: https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/


Tôi cũng đã sử dụng điều này thành công. Tôi thích điều này hơn để phân mảnh sự phụ thuộc vào nuget chưa anohter và bạn thực sự tìm hiểu thêm một chút về những gì đang xảy ra. Điều tuyệt vời là hầu hết các phương pháp cuối cùng vẫn sử dụng SendAsyncnên không cần thiết lập thêm.
Steve Pettifer

4

Tôi không bị thuyết phục bởi nhiều câu trả lời.

Trước hết, hãy tưởng tượng bạn muốn kiểm tra đơn vị một phương pháp sử dụng HttpClient. Bạn không nên khởi tạo HttpClienttrực tiếp trong quá trình triển khai của mình. Bạn nên đặt một nhà máy có trách nhiệm cung cấp một phiên bản HttpClientcho bạn. Bằng cách đó, bạn có thể mô phỏng sau này tại nhà máy đó và trả lại bất kỳ thứ gì HttpClientbạn muốn (ví dụ: mô phỏng HttpClientchứ không phải sản phẩm thật).

Vì vậy, bạn sẽ có một nhà máy như sau:

public interface IHttpClientFactory
{
    HttpClient Create();
}

Và một triển khai:

public class HttpClientFactory
    : IHttpClientFactory
{
    public HttpClient Create()
    {
        var httpClient = new HttpClient();
        return httpClient;
    }
}

Tất nhiên, bạn sẽ cần phải đăng ký trong IoC Container triển khai này. Nếu bạn sử dụng Autofac, nó sẽ giống như sau:

builder
    .RegisterType<IHttpClientFactory>()
    .As<HttpClientFactory>()
    .SingleInstance();

Bây giờ bạn sẽ có một triển khai phù hợp và có thể kiểm tra được. Hãy tưởng tượng rằng phương pháp của bạn giống như sau:

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

Bây giờ là phần thử nghiệm. HttpClientmở rộng HttpMessageHandler, đó là trừu tượng. Hãy tạo một "mock" HttpMessageHandlerchấp nhận một đại biểu để khi chúng ta sử dụng mock, chúng ta cũng có thể thiết lập từng hành vi cho mỗi bài kiểm tra.

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

Và bây giờ, với sự trợ giúp của Moq (và FluentAssertions, một thư viện giúp các bài kiểm tra đơn vị dễ đọc hơn), chúng tôi có mọi thứ cần thiết để kiểm tra đơn vị phương pháp PostAsync sử dụng 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);
        }
    }
}

Rõ ràng là bài kiểm tra này thật ngớ ngẩn, và chúng tôi thực sự đang kiểm tra mô hình của mình. Nhưng bạn hiểu ý rồi đấy. Bạn nên kiểm tra logic có ý nghĩa tùy thuộc vào cách triển khai của bạn, chẳng hạn như ..

  • nếu trạng thái mã của phản hồi không phải là 201, nó có nên ném một ngoại lệ không?
  • nếu văn bản phản hồi không thể được phân tích cú pháp, điều gì sẽ xảy ra?
  • Vân vân.

Mục đích của câu trả lời này là để kiểm tra thứ gì đó sử dụng HttpClient và đây là một cách tốt để làm như vậy.


4

Tham gia nhóm hơi muộn, nhưng tôi thích sử dụng wiremocking ( https://github.com/WireMock-Net/WireMock.Net ) bất cứ khi nào có thể trong quá trình tích hợp vi dịch vụ lõi dotnet với các phụ thuộc REST hạ nguồn.

Bằng cách triển khai TestHttpClientFactory mở rộng IHttpClientFactory, chúng tôi có thể ghi đè phương thức

HttpClient CreateClient (tên chuỗi)

Vì vậy, khi sử dụng các ứng dụng khách được đặt tên trong ứng dụng của mình, bạn có quyền kiểm soát việc trả lại HttpClient có dây cho wiremock của bạn.

Điều tốt về cách tiếp cận này là bạn không thay đổi bất kỳ điều gì trong ứng dụng bạn đang thử nghiệm và cho phép các thử nghiệm tích hợp khóa học thực hiện một yêu cầu REST thực tế đối với dịch vụ của bạn và chế nhạo json (hoặc bất cứ điều gì) mà yêu cầu xuôi dòng thực tế sẽ trả về. Điều này dẫn đến các bài kiểm tra ngắn gọn và càng ít chế nhạo càng tốt trong ứng dụng của bạn.

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

2

HttpClientsử dụng SendAsyncphương pháp để thực hiện tất cả HTTP Requests, bạn có thể override SendAsyncphương pháp và mô phỏng HttpClient.

Đối với bọc đó tạo HttpClientthành a interface, một cái gì đó giống như bên dưới

public interface IServiceHelper
{
    HttpClient GetClient();
}

Sau đó, sử dụng ở trên interfaceđể tiêm phụ thuộc vào dịch vụ của bạn, mẫu bên dưới

public class SampleService
{
    private readonly IServiceHelper serviceHelper;

    public SampleService(IServiceHelper serviceHelper)
    {
        this.serviceHelper = serviceHelper;
    }

    public async Task<HttpResponseMessage> Get(int dummyParam)
    {
        try
        {
            var dummyUrl = "http://www.dummyurl.com/api/controller/" + dummyParam;
            var client = serviceHelper.GetClient();
            HttpResponseMessage response = await client.GetAsync(dummyUrl);               

            return response;
        }
        catch (Exception)
        {
            // log.
            throw;
        }
    }
}

Bây giờ trong dự án thử nghiệm đơn vị, hãy tạo một lớp trợ giúp để chế nhạo SendAsync. Đây là một FakeHttpResponseHandlerlớp inheriting DelegatingHandlersẽ cung cấp một tùy chọn để ghi đè SendAsyncphương thức. Sau khi ghi đè SendAsyncphương thức cần thiết lập một phản hồi cho mỗi phương thức HTTP Requestđang gọi SendAsync, đối với phương thức đó, hãy tạo một Dictionaryvới keyas Urivaluenhư HttpResponseMessagevậy bất cứ khi nào có một HTTP Requestvà nếu các Urikết quả phù hợp SendAsyncsẽ trả về cấu hình HttpResponseMessage.

public class FakeHttpResponseHandler : DelegatingHandler
{
    private readonly IDictionary<Uri, HttpResponseMessage> fakeServiceResponse;
    private readonly JavaScriptSerializer javaScriptSerializer;
    public FakeHttpResponseHandler()
    {
        fakeServiceResponse =  new Dictionary<Uri, HttpResponseMessage>();
        javaScriptSerializer =  new JavaScriptSerializer();
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    public void AddFakeServiceResponse(Uri uri, HttpResponseMessage httpResponseMessage)
    {
        fakeServiceResponse.Remove(uri);
        fakeServiceResponse.Add(uri, httpResponseMessage);
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation having query string parameter.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    /// <param name="requestParameter">Query string parameter.</param>
    public void AddFakeServiceResponse<TQueryStringParameter>(Uri uri, HttpResponseMessage httpResponseMessage, TQueryStringParameter requestParameter)
    {
        var serilizedQueryStringParameter = javaScriptSerializer.Serialize(requestParameter);
        var actualUri = new Uri(string.Concat(uri, serilizedQueryStringParameter));
        fakeServiceResponse.Remove(actualUri);
        fakeServiceResponse.Add(actualUri, httpResponseMessage);
    }

    // all method in HttpClient call use SendAsync method internally so we are overriding that method here.
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if(fakeServiceResponse.ContainsKey(request.RequestUri))
        {
            return Task.FromResult(fakeServiceResponse[request.RequestUri]);
        }

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
        {
            RequestMessage = request,
            Content = new StringContent("Not matching fake found")
        });
    }
}

Tạo một triển khai mới cho IServiceHelperbằng cách mô phỏng khuôn khổ hoặc như bên dưới. FakeServiceHelperLớp này chúng ta có thể sử dụng để tiêm FakeHttpResponseHandlerlớp để bất cứ khi nào HttpClientđược tạo bởi lớp này, classnó sẽ sử dụng FakeHttpResponseHandler classthay vì triển khai thực tế.

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

    public FakeServiceHelper(DelegatingHandler delegatingHandler)
    {
        this.delegatingHandler = delegatingHandler;
    }

    public HttpClient GetClient()
    {
        return new HttpClient(delegatingHandler);
    }
}

Và trong cấu hình thử nghiệm FakeHttpResponseHandler classbằng cách thêm Urivà mong đợi HttpResponseMessage. Các Urinên là thực tế servicethiết bị đầu cuối Uriđể khi overridden SendAsyncphương pháp được gọi là từ thực tế servicetriển khai nó sẽ phù hợp với UriDictionaryvà phản ứng với các cấu hình HttpResponseMessage. Sau khi định cấu hình, hãy tiêm FakeHttpResponseHandler objectvào IServiceHelperthực thi giả mạo . Sau đó, tiêm FakeServiceHelper classdịch vụ thực tế sẽ làm cho dịch vụ thực tế sử dụng override SendAsyncphương pháp này.

[TestClass]
public class SampleServiceTest
{
    private FakeHttpResponseHandler fakeHttpResponseHandler;

    [TestInitialize]
    public void Initialize()
    {
        fakeHttpResponseHandler = new FakeHttpResponseHandler();
    }

    [TestMethod]
    public async Task GetMethodShouldReturnFakeResponse()
    {
        Uri uri = new Uri("http://www.dummyurl.com/api/controller/");
        const int dummyParam = 123456;
        const string expectdBody = "Expected Response";

        var expectedHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(expectdBody)
        };

        fakeHttpResponseHandler.AddFakeServiceResponse(uri, expectedHttpResponseMessage, dummyParam);

        var fakeServiceHelper = new FakeServiceHelper(fakeHttpResponseHandler);

        var sut = new SampleService(fakeServiceHelper);

        var response = await sut.Get(dummyParam);

        var responseBody = await response.Content.ReadAsStringAsync();

        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual(expectdBody, responseBody);
    }
}

Liên kết GitHub: đang có triển khai mẫu


Mặc dù mã này có thể giải quyết câu hỏi, bao gồm giải thích về cách thức và lý do tại sao điều này giải quyết vấn đề sẽ thực sự giúp cải thiện chất lượng bài đăng của bạn và có thể dẫn đến nhiều phiếu bầu hơn. Hãy nhớ rằng bạn đang trả lời câu hỏi cho độc giả trong tương lai, không chỉ người hỏi bây giờ. Vui lòng chỉnh sửa câu trả lời của bạn để thêm giải thích và đưa ra dấu hiệu về những giới hạn và giả định áp dụng.
Богдан Опир

Cảm ơn bạn @ БогданОпир vì phản hồi đã cập nhật giải thích.
ghosh-arun

1

Bạn có thể sử dụng thư viện RichardSzalay MockHttp để chế nhạo HttpMessageHandler và có thể trả về một đối tượng HttpClient sẽ được sử dụng trong quá trình kiểm tra.

GitHub MockHttp

PM> Gói cài đặt RichardSzalay.MockHttp

Từ tài liệu GitHub

MockHttp định nghĩa một HttpMessageHandler thay thế, công cụ điều khiển HttpClient, cung cấp một API cấu hình thông thạo và cung cấp một phản hồi soạn trước. Người gọi (ví dụ: lớp dịch vụ của ứng dụng của bạn) vẫn không biết về sự hiện diện của nó.

Ví dụ từ GitHub

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

1

Đây là một câu hỏi cũ, nhưng tôi cảm thấy thôi thúc phải mở rộng câu trả lời với một giải pháp mà tôi không thấy ở đây.
Bạn có thể giả mạo tập hợp Microsoft (System.Net.Http) và sau đó sử dụng ShinsContext trong quá trình kiểm tra.

  1. Trong VS 2017, nhấp chuột phải vào cụm System.Net.Http và chọn "Add Fakes Assembly"
  2. Đặt mã của bạn trong phương thức kiểm tra đơn vị dưới ShimsContext.Create () bằng cách sử dụng. Bằng cách này, bạn có thể cô lập mã nơi bạn định giả mạo HttpClient.
  3. Tùy thuộc vào việc triển khai và thử nghiệm của bạn, tôi khuyên bạn nên triển khai tất cả các hành động mong muốn mà bạn gọi một phương thức trên HttpClient và muốn giả mạo giá trị trả về. Sử dụng ShimHttpClient.AllInstances sẽ giả mạo việc triển khai của bạn trong tất cả các trường hợp được tạo trong quá trình thử nghiệm của bạn. Ví dụ: nếu bạn muốn giả mạo phương thức GetAsync (), hãy làm như sau:

    [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(); 
        }
    }

1

Tôi đã làm một việc rất đơn giản, như tôi đang ở trong môi trường 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);

        ...
    }

}

và mô phỏng là:

    [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(...);
    }

1

Tất cả những gì bạn cần là một phiên bản thử nghiệm của HttpMessageHandlerlớp mà bạn chuyển cho HttpClientctor. Điểm chính là HttpMessageHandlerlớp thử nghiệm của bạn sẽ có một HttpRequestHandlerđại diện mà người gọi có thể đặt và chỉ cần xử lý HttpRequesttheo cách họ muốn.

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

Bạn có thể sử dụng một phiên bản của lớp này để tạo một phiên bản HttpClient cụ thể. Thông qua ủy quyền HttpRequestHandler, bạn có toàn quyền kiểm soát các yêu cầu http gửi đi từ HttpClient.


1

Lấy cảm hứng từ câu trả lời của PointZeroTwo , đây là một mẫu sử dụng NUnitFakeItEasy .

SystemUnderTest trong ví dụ này là lớp mà bạn muốn kiểm tra - không có nội dung mẫu nào được đưa ra cho nó nhưng tôi cho rằng bạn đã có rồi!

[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(...);
    }
}

điều gì sẽ xảy ra nếu tôi muốn truyền một tham số cho phương thức, được đề cập đến với "x.Method.Name" ..?
Shailesh

0

Có lẽ sẽ có một số mã cần thay đổi trong dự án hiện tại của bạn nhưng đối với các dự án mới, bạn hoàn toàn nên cân nhắc sử dụng Flurl.

https://flurl.dev

Nó là một thư viện máy khách HTTP cho .NET với giao diện thông thạo đặc biệt cho phép khả năng kiểm tra đối với mã sử dụng nó để thực hiện các yêu cầu HTTP.

Có rất nhiều mẫu mã trên trang web nhưng tóm lại, bạn sử dụng nó như thế này trong mã của mình.

Thêm công dụng.

using Flurl;
using Flurl.Http;

Gửi yêu cầu nhận và đọc phản hồi.

public async Task SendGetRequest()
{
   var response = await "https://example.com".GetAsync();
   // ...
}

Trong các bài kiểm tra đơn vị, Flurl hoạt động như một mô hình có thể được cấu hình để hoạt động như mong muốn và cũng để xác minh các cuộc gọi đã được thực hiện.

using (var httpTest = new HttpTest())
{
   // Arrange
   httpTest.RespondWith("OK", 200);

   // Act
   await sut.SendGetRequest();

   // Assert
   httpTest.ShouldHaveCalled("https://example.com")
      .WithVerb(HttpMethod.Get);
}

0

Sau khi tìm kiếm cẩn thận, tôi đã tìm ra cách tốt nhất để thực hiện điều này.

    private HttpResponseMessage response;

    [SetUp]
    public void Setup()
    {
        var handlerMock = new Mock<HttpMessageHandler>();

        handlerMock
           .Protected()
           .Setup<Task<HttpResponseMessage>>(
              "SendAsync",
              ItExpr.IsAny<HttpRequestMessage>(),
              ItExpr.IsAny<CancellationToken>())
           // This line will let you to change the response in each test method
           .ReturnsAsync(() => response);

        _httpClient = new HttpClient(handlerMock.Object);

        yourClinet = new YourClient( _httpClient);
    }

Như bạn nhận thấy, tôi đã sử dụng các gói Moq và Moq.Protected.


0

Để thêm 2 xu của tôi. Để mô phỏng các phương thức yêu cầu http cụ thể hoặc Nhận hoặc Đăng. Điều này đã làm việc cho tôi.

mockHttpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(a => a.Method == HttpMethod.Get), ItExpr.IsAny<CancellationToken>())
                                                .Returns(Task.FromResult(new HttpResponseMessage()
                                                {
                                                    StatusCode = HttpStatusCode.OK,
                                                    Content = new StringContent(""),
                                                })).Verifiable();
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.