Μονάδα δοκιμής HttpClient χωρίς κάλτσες (ελληνικά (Greek))

Μονάδα δοκιμής HttpClient χωρίς κάλτσες

Saturday, 29 November 2025

//

5 minute read

Εισαγωγή

Κατά τη δοκιμή κωδικού που χρησιμοποιεί HttpClient, η παραδοσιακή προσέγγιση περιλαμβάνει χλευασμό HttpMessageHandler Ενώ αυτό λειτουργεί, μπορεί να είναι ρήμα, τελετή-βαρύ, και ειλικρινά λίγο άσχημο. DelegatingHandler να δημιουργήσει χειριστές δοκιμών που συμπεριφέρονται σαν πραγματικά τελικά σημεία HTTP.

Σε αυτή τη θέση θα σας δείξω γιατί μπορεί να παραλείψετε εντελώς τους χλευασμούς και να χρησιμοποιήσετε DelegatingHandler για πιο ευανάγνωστο, συντηρούμενο και συμπαγή κωδικό δοκιμής.

Το πρόβλημα με το χτύπημα HttpMessageHandler

Αυτό είναι το τυπικό. HttpMessageHandler Το χλευασμό μοιάζει με τον Μοκ:

var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>(
        "SendAsync",
        ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.ToString().Contains("api/send")),
        ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync((HttpRequestMessage request, CancellationToken cancellationToken) =>
    {
        var requestBody = request.Content?.ReadAsStringAsync(cancellationToken).Result;
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(requestBody ?? "No content", Encoding.UTF8, "application/json")
        };
    });

var client = new HttpClient(mockHandler.Object);

Αυτό έχει πολλά θέματα:

  1. Ρήξη - Πολλά λεβητοστάσια για μια απλή συμπεριφορά
  2. Τελετή προστατευόμενης μεθόδου - Χρειάζεσαι Protected() και ItExpr επειδή SendAsync προστατεύεται
  3. Δύσκολο να το διαβάσεις. - Η πραγματική λογική δοκιμής είναι θαμμένη στην τελετή εγκατάστασης
  4. Δεν μπορεί να χρησιμοποιηθεί εκ νέου - Κάθε δοκιμή χρειάζεται παρόμοιο κωδικό ρύθμισης
  5. ΜπρίτλCity name (optional, probably does not need a translation) - Εύκολο να πάρεις λάθος το όνομα της μεθόδου που βασίζεται σε συμβολοσειρές

Το DelegatingHandler Εναλλακτικό

DelegatingHandler είναι μια ενσωματωμένη τάξη .NET που έχει σχεδιαστεί για ακριβώς αυτό το σκοπό - αναχαιτίζοντας αιτήματα HTTP πριν χτυπήσει το δίκτυο.

Εδώ είναι η ίδια λειτουργικότητα με τη χρήση DelegatingHandler:

public class EchoHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var content = request.Content != null
            ? await request.Content.ReadAsStringAsync(cancellationToken)
            : "No content";

        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(content, Encoding.UTF8, "application/json")
        };
    }
}

Χρησιμοποιώντας αυτό:

var client = new HttpClient(new EchoHandler());

Χωρίς πλαίσια κοροϊδίας, χωρίς προστατευμένη μέθοδο γυμναστικής, χωρίς ονόματα μεθόδων που βασίζονται σε χορδές.

Ένα πραγματικό-Παγκόσμιο Παράδειγμα: Χειριστής μεταφραστικών υπηρεσιών

Εδώ είναι ένα πιο εξεζητημένο παράδειγμα από έναν χειριστή μεταφράσεων:

public class TranslateDelegatingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var absPath = request.RequestUri?.AbsolutePath;
        var method = request.Method;

        return absPath switch
        {
            "/translate" when method == HttpMethod.Post => await HandleTranslate(request),
            "/translate" => new HttpResponseMessage(HttpStatusCode.OK),
            "/health" => new HttpResponseMessage(HttpStatusCode.OK),
            _ => new HttpResponseMessage(HttpStatusCode.NotFound)
        };
    }

    private static async Task<HttpResponseMessage> HandleTranslate(HttpRequestMessage request)
    {
        var content = await request.Content!.ReadFromJsonAsync<TranslateRequest>();

        // Simulate error for specific test case
        if (content?.TargetLanguage == "xx")
            return new HttpResponseMessage(HttpStatusCode.InternalServerError);

        var response = new TranslateResponse("es", new[] { "Texto traducido" });
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = JsonContent.Create(response)
        };
    }
}

Αυτός ο χειριστής:

  • Διαδρομές διαφορετικές διαδρομές σε διαφορετικές συμπεριφορές
  • Απελπίζει το περιεχόμενο της αίτησης για λήψη αποφάσεων
  • Επιστρέφει κατάλληλους κωδικούς σφάλματος για συγκεκριμένα σενάρια
  • Είναι πλήρως ευανάγνωστο και αυτο-ντοκουμέντο

Ρύθμιση με ένεση εξάρτησης

Κατά τη χρήση IHttpClientFactory (που θα έπρεπε να είστε), η ενσωμάτωση των χειριστών δοκιμών είναι απλή:

public static IServiceCollection SetupTestServices(DelegatingHandler handler)
{
    var services = new ServiceCollection();

    services.AddHttpClient<ITranslationService, TranslationService>(client =>
    {
        client.BaseAddress = new Uri("https://test.local");
    })
    .ConfigurePrimaryHttpMessageHandler(() => handler);

    return services;
}

Στη συνέχεια, στις εξετάσεις σας:

[Fact]
public async Task Translate_ReturnsTranslatedText()
{
    var services = SetupTestServices(new TranslateDelegatingHandler());
    var provider = services.BuildServiceProvider();
    var service = provider.GetRequiredService<ITranslationService>();

    var result = await service.TranslateAsync("Hello", "es");

    Assert.Equal("Texto traducido", result);
}

[Fact]
public async Task Translate_InvalidLanguage_ThrowsException()
{
    var services = SetupTestServices(new TranslateDelegatingHandler());
    var provider = services.BuildServiceProvider();
    var service = provider.GetRequiredService<ITranslationService>();

    await Assert.ThrowsAsync<HttpRequestException>(
        () => service.TranslateAsync("Hello", "xx"));
}

Προηγμένο μοτίβο: Ρυθμιζόμενοι χειριστές

Για περισσότερη ευελιξία, μπορείτε να δημιουργήσετε χειριστές που δέχονται ρυθμίσεις:

public class ConfigurableHandler : DelegatingHandler
{
    private readonly Dictionary<string, Func<HttpRequestMessage, Task<HttpResponseMessage>>> _routes;

    public ConfigurableHandler()
    {
        _routes = new Dictionary<string, Func<HttpRequestMessage, Task<HttpResponseMessage>>>();
    }

    public ConfigurableHandler WithRoute(string path, HttpStatusCode status)
    {
        _routes[path] = _ => Task.FromResult(new HttpResponseMessage(status));
        return this;
    }

    public ConfigurableHandler WithRoute(string path, object responseBody)
    {
        _routes[path] = _ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = JsonContent.Create(responseBody)
        });
        return this;
    }

    public ConfigurableHandler WithRoute(
        string path,
        Func<HttpRequestMessage, Task<HttpResponseMessage>> handler)
    {
        _routes[path] = handler;
        return this;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var path = request.RequestUri?.AbsolutePath ?? "";

        if (_routes.TryGetValue(path, out var handler))
            return await handler(request);

        return new HttpResponseMessage(HttpStatusCode.NotFound);
    }
}

Χρήση:

var handler = new ConfigurableHandler()
    .WithRoute("/api/users", new[] { new User("Alice"), new User("Bob") })
    .WithRoute("/api/health", HttpStatusCode.OK)
    .WithRoute("/api/error", HttpStatusCode.InternalServerError);

var client = new HttpClient(handler);

Γιατί να Επιλέξετε την Απονομή Χάντλερ για τις Μοκς;

Φανταστείτε τον Moq-based Mocking DelegatingHandler |--------|-------------------|-------------------| | Γραμμές κωδικών Πολλοί λίγοι λίγοι | Δυνατότητα ανάγνωσης Low (ceremony heavy) | Επαναχρησιμοποίηση Φτωχέ μου Εξαιρετικά | Αποσφαλμάτωση Πιο δυνατά (μαγεία) Εύκολη (βήμα προς τα μέσα) | Αναπροσανατολισμός Μπρίτλ Ρόμπουστ | Καμπύλη εκμάθησης Στεφάνι (Moq APIs) | Εξαρτήσεις ~ Απαιτεί Moq ~ Κανένας (ενσωματωμένος) ~

Όταν η Κόλλησις Εξακολουθεί να Κάνει Αίσθηση

Για να είμαστε δίκαιοι, υπάρχουν σενάρια όπου η κοροϊδία τύπου Moq μπορεί να εξακολουθεί να είναι κατάλληλη:

  1. Μία απλή απάντηση - Αν χρειαστείς έναν χειριστή μόνος σου μια φορά, η ενδοεπικοινωνία Μακ μπορεί να είναι γρηγορότερη.
  2. Επαλήθευση - Μακς. Verify() είναι χρήσιμο για τη διεκδίκηση κλήσεων που πραγματοποιήθηκαν
  3. Υφιστάμενη βάση κωδικών - Αν η ομάδα σας έχει ήδη εκτεταμένη υποδομή Moq

Για επαλήθευση, μπορείτε να το προσθέσετε στο DelegatingHandler πάρα πολύ:

public class VerifyingHandler : DelegatingHandler
{
    public List<HttpRequestMessage> ReceivedRequests { get; } = new();

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        ReceivedRequests.Add(request);
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
    }
}

Συμπέρασμα

Χρήση DelegatingHandler για τη δοκιμή HttpClient σας δίνει:

  • Συμπαγής κωδικός - Χωρίς παρωδία τελετής-πλαίσιο.
  • Αναγνώσιμες δοκιμές - Απλά κανονικά μαθήματα C#
  • Επαναχρησιμοποιούμενοι χειριστές - Μοιραστείτε σε όλες τις κατηγορίες δοκιμών
  • Εύκολη αποσφαλμάτωση - Ορίστε τα σημεία διακοπής, περάστε τον κωδικό
  • Μηδέν εξαρτήσεις - Είναι φτιαγμένο σε .NET

Την επόμενη φορά που θα φτάσεις για Mock<HttpMessageHandler>, σκεφτείτε αν ένα απλό DelegatingHandler Ο μελλοντικός σου εαυτός (και οι συμπαίκτες σου) θα σε ευχαριστήσουν για τον καθαρότερο, πιο συντηρητικό κώδικα δοκιμών.

Δείτε τα δοκιμαστικά σχέδια σε αυτή τη λύση για παραδείγματα σε πραγματικό κόσμο αυτού του μοτίβου σε δράση.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.