Mockowanie HttpClient w testach jednostkowych

111

Mam problemy z opakowaniem kodu do użycia w testach jednostkowych. Problem jest taki. Mam interfejs IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

I klasa, która go używa, HttpHandler:

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

A następnie klasa Connection, która używa simpleIOC do wstrzyknięcia implementacji klienta:

public class Connection
{
    private IHttpHandler _httpClient;

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

A potem mam projekt testu jednostkowego, który ma tę klasę:

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?     

}

Teraz oczywiście będę mieć metody w klasie Connection, które będą pobierać dane (JSON) z mojego zaplecza. Jednak chcę napisać testy jednostkowe dla tej klasy i oczywiście nie chcę pisać testów na prawdziwym zapleczu, a raczej na wyśmiewanym. Próbowałem znaleźć w Google dobrą odpowiedź bez wielkiego sukcesu. Mogłem wcześniej używać Moq do kpiny, ale nigdy na czymś takim jak httpClient. Jak mam podejść do tego problemu?

Z góry dziękuję.

tjugg
źródło
1
Wystąpienie HttpClientw interfejsie jest przyczyną problemu. Zmuszasz klienta do używania HttpClientkonkretnej klasy. Zamiast tego, należy odsłonić abstrakcję z poniższych HttpClient.
Mike Eason
Czy możesz to nieco dokładniej wyjaśnić? Jak mam zbudować konstruktor klas połączeń, ponieważ nie chcę żadnych zależności HttpClient w innych klasach, użyj klasy Connection. Na przykład nie chcę przekazywać Concepret HttpClient w konstruktorze Connection, ponieważ spowodowałoby to, że każda inna klasa, która używa połączenia, byłaby zależna od HttpClient?
tjugg
Co wygooglowałeś? Najwyraźniej mockhttp przydałby się kilka ulepszeń SEO.
Richard Szalay,
@Mike - jak wspomniałem w mojej odpowiedzi, naprawdę nie ma potrzeby abstrahowania HttpClient. Jest to doskonale sprawdzalne w obecnym stanie. Mam wiele projektów, które używają tej metody bez backendowych zestawów testów.
Richard Szalay,

Odpowiedzi:

37

Twój interfejs ujawnia konkretną HttpClientklasę, dlatego wszelkie klasy, które używają tego interfejsu, są z nim powiązane, co oznacza, że ​​nie można z niego wyśmiać.

HttpClientnie dziedziczy z żadnego interfejsu, więc będziesz musiał napisać własny. Proponuję wzór podobny do dekoratora :

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

Twoja klasa będzie wyglądać tak:

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

W tym wszystkim chodzi o to HttpClientHandler tworzy swoje własne HttpClient, możesz wtedy oczywiście stworzyć wiele klas, które implementują się IHttpHandlerna różne sposoby.

Głównym problemem związanym z tym podejściem jest to, że skutecznie piszesz klasę, która po prostu wywołuje metody z innej klasy, jednak możesz utworzyć klasę dziedziczącą po HttpClient(patrz przykład Nkosi , jest to znacznie lepsze podejście niż moje). Życie byłoby o wiele łatwiejsze, gdybyś HttpClientmiał interfejs, z którego można kpić, niestety tak nie jest.

Ten przykład nie jest jednak złotym biletem. IHttpHandlerwciąż opiera się na HttpResponseMessage, który należy do System.Net.Httpprzestrzeni nazw, dlatego jeśli potrzebujesz innych implementacji innych niż HttpClient, będziesz musiał wykonać pewnego rodzaju mapowanie, aby przekonwertować ich odpowiedzi na HttpResponseMessageobiekty. To oczywiście tylko problem , jeśli chcesz korzystać z wielu wdrożeń o IHttpHandlerale to nie wygląda jak to zrobić to nie koniec świata, ale jest to coś do myślenia.

W każdym razie możesz po prostu kpić IHttpHandler nie martwiąc się o konkretną HttpClientklasę, ponieważ została wyabstrahowana.

Zalecam testowanie metod innych niż asynchroniczne , ponieważ nadal wywołują one metody asynchroniczne, ale bez kłopotów związanych z testowaniem jednostkowym metod asynchronicznych, zobacz tutaj

Mike Eason
źródło
To rzeczywiście odpowiada na moje pytanie. Odpowiedź Nkosis jest również poprawna, więc nie jestem pewien, którą powinienem zaakceptować jako odpowiedź, ale pójdę z tym. Dziękuję obojgu za wysiłek
tjugg
@tjugg Cieszę się, że mogę pomóc. Nie wahaj się zagłosować na odpowiedzi, jeśli uznasz je za przydatne.
Nkosi
3
Warto zauważyć, że główna różnica między tą odpowiedzią a odpowiedzią Nkosiego polega na tym, że jest to znacznie cieńsza abstrakcja. Cienki jest prawdopodobnie dobry na skromny przedmiot
Ben Aaronson,
228

Rozszerzalność HttpClient leży w HttpMessageHandlerprzekazaniu do konstruktora. Jego celem jest umożliwienie implementacji specyficznych dla platformy, ale możesz też z tego kpić. Nie ma potrzeby tworzenia otoki dekoratora dla HttpClient.

Jeśli wolisz DSL zamiast Moq, mam bibliotekę na GitHub / Nuget, która trochę ułatwia: 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'}
Richard Szalay
źródło
1
Więc po prostu przekazałbym MockHttpMessageHandler jako klasę messagehandler Httphandler? Albo jak wdrożyliście to we własnych projektach
tjugg
2
Świetna odpowiedź i coś, czego początkowo bym nie wiedział. Sprawia, że ​​praca z HttpClient nie jest taka zła.
Bealer
6
Dla ludzi, którzy nie chcą zajmować się wstrzykiwaniem klienta, ale nadal chcą łatwego testowania, jest to trywialne do osiągnięcia. Wystarczy wymienić var client = new HttpClient()z var client = ClientFactory()i konfiguracja polu internal static Func<HttpClient> ClientFactory = () => new HttpClient();i na poziomie testu można przepisać to pole.
Chris Marisic
3
@ChrisMarisic sugerujesz formę serwisu w celu wymiany wtrysku. Lokalizacja usługi jest dobrze znanym anty-wzorcem, więc preferowane jest wstrzyknięcie imho.
MarioDS
2
@MarioDS i niezależnie od tego, nie powinieneś w ogóle wstrzykiwać wystąpienia HttpClient . Jeśli jesteś zdeterminowany, aby użyć do tego zastrzyku konstruktora, powinieneś wstrzyknąć HttpClientFactoryjak w Func<HttpClient>. Biorąc pod uwagę, że postrzegam HttpClient jako czysto szczegół implementacyjny, a nie zależność, użyję statystyk, tak jak zilustrowałem powyżej. Nie mam nic przeciwko testom manipulującym elementami wewnętrznymi. Jeśli zależy mi na czystym izmie, postawię pełne serwery i przetestuję ścieżki kodu na żywo. Używanie wszelkiego rodzaju szyderstw oznacza, że ​​akceptujesz przybliżone zachowanie, a nie rzeczywiste zachowanie.
Chris Marisic
40

Zgadzam się z niektórymi innymi odpowiedziami, że najlepszym podejściem jest mockowanie HttpMessageHandler zamiast zawijania HttpClient. Ta odpowiedź jest wyjątkowa, ponieważ nadal wstrzykuje HttpClient, dzięki czemu może być pojedynczą lub zarządzaną za pomocą iniekcji zależności.

HttpClient ma być utworzony raz i ponownie użyty przez cały okres istnienia aplikacji.

( Źródło ).

Mockowanie HttpMessageHandler może być trochę trudne, ponieważ SendAsync jest chroniony. Oto kompletny przykład, używając xunit i 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);
        }
    }
}
PointZeroTwo
źródło
1
Bardzo mi się to podobało. To mockMessageHandler.Protected()był zabójca. Dzięki za ten przykład. Pozwala na napisanie testu bez jakiejkolwiek modyfikacji źródła.
tyrion
1
FYI, Moq 4.8 obsługuje silnie wpisane kpiny z chronionych członków - github.com/Moq/moq4/wiki/Quickstart
Richard Szalay
2
Wygląda świetnie. Również Moq obsługuje ReturnsAsync, więc kod wyglądałby tak.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord
Dzięki @kord, dodałem to do odpowiedzi
PointZeroTwo
3
Czy istnieje sposób, aby sprawdzić, czy „SandAsync” została wywołana z niektórymi parametrami? Próbowałem użyć ... Protected () .Weryfikuj (...), ale wygląda na to, że nie działa z metodami asynchronicznymi.
Rroman
29

To jest częste pytanie i byłem mocno po stronie, chcąc kpić z HttpClient, ale myślę, że w końcu doszedłem do wniosku, że nie powinieneś kpić z HttpClient. Wydaje się to logiczne, ale myślę, że zrobiono nam pranie mózgu przez rzeczy, które widzimy w bibliotekach open source.

Często widzimy „klientów”, których mockujemy w naszym kodzie, abyśmy mogli testować w izolacji, więc automatycznie próbujemy zastosować tę samą zasadę do HttpClient. HttpClient faktycznie dużo robi; możesz myśleć o nim jako o menedżerze HttpMessageHandler, więc nie chcesz z tego kpić i dlatego nadal nie ma interfejsu. Część że jesteś naprawdę zainteresowany dla testów jednostkowych lub projektując swoje usługi, nawet, jest HttpMessageHandler ponieważ to właśnie zwraca odpowiedź, a ty możesz kpić, że.

Warto również zaznaczyć, że prawdopodobnie powinieneś zacząć traktować HttpClient jak większą transakcję. Na przykład: ogranicz do minimum tworzenie nowych HttpClients. Używaj ich ponownie, są zaprojektowane tak, aby można je było ponownie wykorzystać i zużywać o wiele mniej zasobów, jeśli to zrobisz. Jeśli zaczniesz traktować to jak większą transakcję, poczujesz się znacznie bardziej źle, chcąc z niej kpić, a teraz program obsługi wiadomości zacznie być tym, co wstrzykujesz, a nie klientem.

Innymi słowy, zaprojektuj zależności wokół programu obsługi zamiast klienta. Jeszcze lepiej, abstrakcyjne „usługi”, które używają HttpClient, które pozwalają na wstrzyknięcie programu obsługi i zamiast tego używają go jako zależności do wstrzykiwania. Następnie w swoich testach możesz sfałszować program obsługi, aby kontrolować reakcję na konfigurację testów.

Pakowanie HttpClient to szalona strata czasu.

Aktualizacja: zobacz przykład Joshua Dooms. To jest dokładnie to, co polecam.

Sinaesthetic
źródło
17

Jak wspomniano również w komentarzach, musisz usunąć plikHttpClient aby nie być z nim powiązanym. W przeszłości zrobiłem coś podobnego. Spróbuję dostosować to, co zrobiłem do tego, co ty próbujesz zrobić.

Najpierw spójrz na HttpClient klasie i zdecyduj, jaką funkcjonalność zapewnia, która będzie potrzebna.

Oto możliwość:

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

Ponownie, jak wspomniano wcześniej, dotyczyło to szczególnych celów. Całkowicie odrzuciłem większość zależności od wszystkiego, czym się zajmowałem, HttpClienti skupiłem się na tym, co chciałem zwrócić. Powinieneś ocenić, jak chcesz wyodrębnićHttpClient aby zapewnić tylko niezbędną funkcjonalność, której potrzebujesz.

Umożliwi to teraz wyśmiewanie tylko tego, co jest potrzebne do przetestowania.

Poleciłbym nawet IHttpHandlercałkowite zniesienie i użycie HttpClientabstrakcji IHttpClient. Ale po prostu nie wybieram, ponieważ możesz zastąpić treść interfejsu programu obsługi członkami wyabstrahowanego klienta.

Implementację tego IHttpClientmożna następnie użyć do opakowania / dostosowania rzeczywistego / konkretnego HttpClientlub dowolnego innego obiektu w tym celu, który może być użyty do wysyłania żądań HTTP, ponieważ to, czego naprawdę chciałeś, to usługa zapewniająca tę funkcjonalność zgodnie zHttpClient konkretnym przypadku. Korzystanie z abstrakcji jest czystym (moim zdaniem) i SOLIDnym podejściem i może sprawić, że kod będzie łatwiejszy w utrzymaniu, jeśli zajdzie potrzeba zmiany klienta bazowego na coś innego, gdy zmieni się struktura.

Oto fragment pokazujący, jak można wykonać implementację.

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

Jak widać na powyższym przykładzie, HttpClientza abstrakcją ukryta jest duża część ciężkiego podnoszenia zwykle związanego z używaniem .

Klasę połączenia można następnie wstrzyknąć za pomocą abstrakcyjnego klienta

public class Connection
{
    private IHttpClient _httpClient;

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

Twój test może następnie kpić z tego, co jest potrzebne do testu 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();  
}
Nkosi
źródło
TO jest odpowiedź. Abstrakcja powyżej HttpClienti adapter do tworzenia konkretnej instancji przy użyciu HttpClientFactory. Dzięki temu testowanie logiki wykraczającej poza żądanie HTTP jest trywialne, co jest tutaj celem.
pimbrouwers
13

Opierając się na innych odpowiedziach, proponuję ten kod, który nie ma żadnych zewnętrznych zależności:

[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);
    }
}
pius
źródło
4
Skutecznie testujesz swoją próbę. Prawdziwą mocą kpiny jest to, że możesz ustawić oczekiwania i zmienić jego zachowanie w każdym teście. Fakt, że musisz coś wdrożyć HttpMessageHandlersamemu, sprawia, że ​​jest to prawie niemożliwe - a musisz to zrobić, ponieważ metody są protected internal.
MarioDS
3
@MarioDS Myślę, że chodzi o to, że możesz kpić z odpowiedzi HTTP, aby móc przetestować resztę kodu. Jeśli wstrzykniesz fabrykę, która pobiera HttpClient, w testach możesz podać ten HttpClient.
chris31389
13

Myślę, że problem polega na tym, że masz to trochę do góry nogami.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

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

Jeśli spojrzysz na powyższą klasę, myślę, że tego właśnie chcesz. Firma Microsoft zaleca utrzymywanie klienta przy życiu w celu uzyskania optymalnej wydajności, więc ten typ struktury pozwala na to. Również HttpMessageHandler jest klasą abstrakcyjną i dlatego można ją kopiować. Twoja metoda testowa wyglądałaby wtedy następująco:

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

Pozwala to przetestować logikę podczas mockowania zachowania HttpClient.

Przepraszam, po napisaniu tego i wypróbowaniu tego samemu zdałem sobie sprawę, że nie można kpić z metod chronionych w HttpMessageHandler. Następnie dodałem następujący kod, aby umożliwić wstrzyknięcie odpowiedniego makiety.

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

Testy napisane w ten sposób wyglądają mniej więcej tak:

[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
}
Joshua Dooms
źródło
9

Jeden z moich kolegów zauważył, że większość HttpClientmetod wywołuje SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)pod maską, co jest metodą wirtualną z HttpMessageInvoker:

Zdecydowanie najłatwiejszym sposobem wyśmiewania się HttpClientbyło po prostu kpienie z tej konkretnej metody:

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

a twój kod może wywoływać większość (ale nie wszystkie) HttpClientmetod klas, w tym zwykłą

httpClient.SendAsync(req)

Kliknij tutaj, aby potwierdzić https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs

Adam
źródło
1
Nie działa to jednak dla żadnego kodu, który wywołuje SendAsync(HttpRequestMessage)bezpośrednio. Jeśli możesz zmodyfikować swój kod, aby nie korzystał z tej wygodnej funkcji, to bezpośrednie mockowanie HttpClient przez zastąpienie SendAsyncjest w rzeczywistości najczystszym rozwiązaniem, jakie znalazłem.
Dylan Nicholson
8

Jedną z alternatyw byłoby skonfigurowanie pośredniczącego serwera HTTP, który zwraca gotowe odpowiedzi na podstawie wzorca pasującego do adresu URL żądania, co oznacza, że ​​testujesz rzeczywiste żądania HTTP, a nie fałszywe. W przeszłości wymagałoby to znacznego wysiłku i byłoby zbyt wolne, aby wziąć pod uwagę testy jednostkowe, jednak biblioteka WireMock.net OSS jest łatwa w użyciu i wystarczająco szybka, aby można ją było uruchomić z wieloma testami, więc warto ją rozważyć. Konfiguracja to kilka wierszy kodu:

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

Więcej szczegółów i wskazówek dotyczących używania wiremock w testach można znaleźć tutaj.

alastairtree
źródło
8

Oto proste rozwiązanie, które dobrze mi się sprawdziło.

Korzystanie z biblioteki symulującej 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>()
);

Źródło: https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/

j7nn7k
źródło
Z tego też korzystałem. Wolę to niż wtrącanie się w jeszcze bardziej nugetową zależność, a tak naprawdę dowiesz się trochę więcej o tym, co dzieje się pod maską. Fajną rzeczą jest to, że większość metod i tak kończy się użyciem, SendAsyncwięc nie jest wymagana dodatkowa konfiguracja.
Steve Pettifer
4

Wiele odpowiedzi mnie nie przekonuje.

Przede wszystkim wyobraź sobie, że chcesz przeprowadzić test jednostkowy metody, która używa HttpClient. Nie należy tworzyć instancji HttpClientbezpośrednio w implementacji. Powinieneś wstrzyknąć fabrykę odpowiedzialną za dostarczenie HttpClientdla ciebie instancji . W ten sposób możesz później kpić z tej fabryki i zwrócić cokolwiek HttpClientzechcesz (np: próbną, HttpClienta nie prawdziwą).

Więc miałbyś taką fabrykę jak poniżej:

public interface IHttpClientFactory
{
    HttpClient Create();
}

I realizacja:

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

Oczywiście musisz zarejestrować tę implementację w swoim IoC Container. Jeśli używasz Autofac, byłoby to coś takiego:

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

Teraz miałbyś właściwą i możliwą do przetestowania implementację. Wyobraź sobie, że Twoja metoda to coś takiego:

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

Teraz część testowa. HttpClientrozszerza HttpMessageHandler, co jest abstrakcyjne. Stwórzmy "mock" tego, HttpMessageHandlerktóry akceptuje delegata, więc kiedy używamy mocka, możemy również ustawić każde zachowanie dla każdego testu.

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

A teraz, z pomocą Moq (i FluentAssertions, biblioteki, która sprawia, że ​​testy jednostkowe są bardziej czytelne), mamy wszystko, co potrzebne do testów jednostkowych naszej metody PostAsync, która używa 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);
        }
    }
}

Oczywiście ten test jest głupi i naprawdę testujemy naszą próbę. Ale masz pomysł. W zależności od implementacji należy przetestować sensowną logikę, na przykład ...

  • jeśli stan kodu odpowiedzi jest inny niż 201, czy powinien zgłosić wyjątek?
  • jeśli nie można przeanalizować tekstu odpowiedzi, co powinno się stać?
  • itp.

Celem tej odpowiedzi było przetestowanie czegoś, co używa HttpClient i jest to ładny, czysty sposób na zrobienie tego.

diegosasw
źródło
4

Dołączam do imprezy trochę późno, ale lubię używać wiremocking ( https://github.com/WireMock-Net/WireMock.Net ), gdy tylko jest to możliwe w teście integracji mikrousługi dotnet core z podrzędnymi zależnościami REST.

Implementując TestHttpClientFactory, rozszerzając IHttpClientFactory, możemy przesłonić metodę

HttpClient CreateClient (nazwa ciągu)

Dlatego podczas korzystania z nazwanych klientów w aplikacji masz kontrolę nad zwróceniem HttpClient połączonego z wiremockiem.

Dobrą rzeczą w tym podejściu jest to, że nie zmieniasz niczego w testowanej aplikacji i umożliwia testom integracji kursu wykonanie rzeczywistego żądania REST do Twojej usługi i mockowanie json (lub cokolwiek innego), które powinno zwrócić rzeczywiste żądanie podrzędne. Prowadzi to do zwięzłych testów i jak najmniejszego mockowania w Twojej aplikacji.

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

i

// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);
Markus Foss
źródło
2

Ponieważ HttpClientużyj SendAsyncmetody, aby wykonać wszystko HTTP Requests, możesz override SendAsyncmetodę i kpić z HttpClient.

Za które owijają tworzenia HttpClientDo interface, coś jak poniżej

public interface IServiceHelper
{
    HttpClient GetClient();
}

Następnie użyj powyższego interfacedo iniekcji zależności w usłudze, przykład poniżej

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

Teraz w projekcie testów jednostkowych utwórz klasę pomocniczą do mockowania SendAsync. Tutaj jest to FakeHttpResponseHandlerklasa, która inheriting DelegatingHandlerzapewni opcję nadpisania SendAsyncmetody. Po zastąpieniu SendAsyncmetody należy ustawić odpowiedź dla każdego, HTTP Requestktóry wywołuje SendAsyncmetodę, w tym celu utwórz Dictionaryz keyas Urii valueas HttpResponseMessagetak, aby zawsze, gdy istnieje HTTP Requesti jeśli Uridopasowanie SendAsynczwróci skonfigurowane 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")
        });
    }
}

Utwórz nową implementację IServiceHelperprzez mockowanie frameworka lub jak poniżej. Tej FakeServiceHelperklasy możemy użyć do wstrzyknięcia FakeHttpResponseHandlerklasy, aby za każdym razem, gdy HttpClientutworzona przez to class, używała FakeHttpResponseHandler classzamiast rzeczywistej implementacji.

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

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

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

W konfiguracji testowej FakeHttpResponseHandler class, dodając Urii oczekiwane HttpResponseMessage. UriPowinien być rzeczywisty servicekońcowy Uritak, że gdy overridden SendAsyncmetoda nazywa się od rzeczywistej servicerealizacji będzie odpowiadać Uriw Dictionaryodpowiedzi ze skonfigurowanym HttpResponseMessage. Po skonfigurowaniu wstrzyknij FakeHttpResponseHandler objectdo fałszywej IServiceHelperimplementacji. Następnie wstrzyknij the FakeServiceHelper classdo rzeczywistej usługi, która spowoduje, że rzeczywista usługa będzie używać override SendAsyncmetody.

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

GitHub Link: który ma przykładową implementację

ghosh-arun
źródło
Chociaż ten kod może rozwiązać problem, w tym wyjaśnienie, jak i dlaczego to rozwiązuje problem, naprawdę pomogłoby poprawić jakość twojego posta i prawdopodobnie zaowocowałoby większą liczbą pozytywnych głosów. Pamiętaj, że odpowiadasz na pytanie do czytelników w przyszłości, a nie tylko osoba, która zapyta teraz. Proszę edytować swoje odpowiedzi, aby dodać wyjaśnień i dać wskazówkę co zastosować ograniczenia i założenia.
Богдан Опир
Dziękuję @ БогданОпир za opinie zaktualizowane wyjaśnienie.
ghosh-arun
1

Możesz użyć biblioteki RichardSzalay MockHttp, która mockuje HttpMessageHandler i może zwrócić obiekt HttpClient do użycia podczas testów.

GitHub MockHttp

PM> Zainstaluj pakiet RichardSzalay.MockHttp

Z dokumentacji GitHub

MockHttp definiuje zastępczy HttpMessageHandler, aparat sterujący HttpClient, który zapewnia płynny interfejs API do konfiguracji i zapewnia gotową odpowiedź. Dzwoniący (np. Warstwa usług Twojej aplikacji) pozostaje nieświadomy jej obecności.

Przykład z 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'}
Justin
źródło
1

To stare pytanie, ale czuję potrzebę poszerzenia odpowiedzi o rozwiązanie, którego tutaj nie widziałem.
Możesz sfałszować asembler Microsoft (System.Net.Http), a następnie użyć ShinsContext podczas testu.

  1. W VS 2017 kliknij prawym przyciskiem zestaw System.Net.Http i wybierz „Dodaj fałszywy zestaw”
  2. Umieść swój kod w metodzie testów jednostkowych w ShimsContext.Create () przy użyciu. W ten sposób możesz odizolować kod, w którym planujesz sfałszować HttpClient.
  3. W zależności od implementacji i testu sugerowałbym zaimplementowanie wszystkich pożądanych działań, w których wywołujesz metodę na HttpClient i chcesz sfałszować zwróconą wartość. Użycie ShimHttpClient.AllInstances sfałszuje Twoją implementację we wszystkich instancjach utworzonych podczas testu. Na przykład, jeśli chcesz sfałszować metodę GetAsync (), wykonaj następujące czynności:

    [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(); 
        }
    }
Luca
źródło
1

Zrobiłem coś bardzo prostego, ponieważ byłem w środowisku 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);

        ...
    }

}

a mock to:

    [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(...);
    }
Jorge Aguilar
źródło
1

Potrzebujesz tylko testowej wersji HttpMessageHandlerklasy, którą przekazujesz HttpClientctorowi. Najważniejsze jest to, że twoja HttpMessageHandlerklasa testowa będzie miała HttpRequestHandlerdelegata, którego wywołujący mogą ustawić i po prostu obsługiwać HttpRequesttak, jak chcą.

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

Możesz użyć wystąpienia tej klasy, aby utworzyć konkretne wystąpienie HttpClient. Poprzez delegata HttpRequestHandler masz pełną kontrolę nad wychodzącymi żądaniami http z HttpClient.

Dogu Arslan
źródło
1

Zainspirowany odpowiedzią PointZeroTwo , oto próbka wykorzystująca NUnit i FakeItEasy .

SystemUnderTest w tym przykładzie jest to klasa, którą chcesz przetestować - nie podano przykładowej treści, ale zakładam, że już ją masz!

[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(...);
    }
}
thinkOfaNumber
źródło
co się stanie, jeśli chcę przekazać parametr do metody wspomnianej przy „x.Method.Name” ..?
Shailesh
0

Być może byłby jakiś kod do zmiany w twoim obecnym projekcie, ale w przypadku nowych projektów zdecydowanie powinieneś rozważyć użycie Flurl.

https://flurl.dev

Jest to biblioteka klienta HTTP dla .NET z płynnym interfejsem, który w szczególności umożliwia testowanie kodu używającego go do wykonywania żądań HTTP.

Na stronie jest mnóstwo przykładów kodu, ale w skrócie używasz go w swoim kodzie w ten sposób.

Dodaj zastosowania.

using Flurl;
using Flurl.Http;

Wyślij żądanie odbioru i przeczytaj odpowiedź.

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

W testach jednostkowych Flurl działa jako makieta, którą można skonfigurować tak, aby zachowywała się zgodnie z oczekiwaniami, a także weryfikowała wykonane wywołania.

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

   // Act
   await sut.SendGetRequest();

   // Assert
   httpTest.ShouldHaveCalled("https://example.com")
      .WithVerb(HttpMethod.Get);
}
Christian Kadluba
źródło
0

Po dokładnych poszukiwaniach znalazłem najlepsze podejście, aby to osiągnąć.

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

Jak zauważyłeś, korzystałem z pakietów Moq i Moq.Protected.

Amin Mohamed
źródło
0

Aby dodać moje 2 centy. Aby udawać określone metody żądania http, Get lub Post. To zadziałało dla mnie.

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();
sam
źródło