Einheitenprüfung Umami-.-Net Testen von Umamidaten ohne Verwendung von Moq (Deutsch (German))

Einheitenprüfung Umami-.-Net Testen von Umamidaten ohne Verwendung von Moq

Wednesday, 04 September 2024

//

8 minute read

Einleitung

Im vorherigen Teil dieser Serie, wo ich getestet Umami-.-Net-Tracking Methoden

Das Problem

Im vorherigen Teil habe ich Moq benutzt, um mir eine Mock<HttpMessageHandler> und den benutzten Handler zurückgeben UmamiClientDies ist ein häufiges Muster beim Testen von Code, das HttpClientIn diesem Beitrag werde ich Ihnen zeigen, wie man die neue testen UmamiDataService ohne Verwendung von Moq-.

    public static HttpMessageHandler Create()
    {
        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) =>
            {
                // Read the request content
                var requestBody = request.Content != null
                    ? request.Content.ReadAsStringAsync(cancellationToken).Result
                    : null;

                // Create a response that echoes the request body
                var responseContent = requestBody != null
                    ? requestBody
                    : "No request body";


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

        return mockHandler.Object;
    }

Warum verwenden Sie Moq-??

Moq ist eine leistungsstarke Spoting-Bibliothek, mit der Sie Mock-Objekte für Schnittstellen und Klassen erstellen können.Es wird weit verbreitet in Unit-Tests verwendet, um den Code unter Test von seinen Abhängigkeiten zu isolieren..... Jedoch gibt es einige Fälle, in denen die Verwendung von Moq schwerfällig oder sogar unmöglich sein kann.

Das Beispiel, das ich oben gab, gibt eine Menge Flexibilität beim Testen der UmamiClient Klasse - ,, aber es hat auch einige Nachteile , . , Es ist UGLY-Code und tut eine Menge von Sachen, die ich nicht wirklich brauchen , . Also beim Testen UmamiDataService Ich entschied mich, einen anderen Ansatz zu versuchen.

Testen von UmamiDataService

Das UmamiDataService ist eine zukünftige Ergänzung zu der Umami-.-Net-Bibliothek, die Ihnen erlauben wird, Daten von Umami für Sachen wie sehen, wie viele Ansichten eine Seite hatte ., , welche Ereignisse eines bestimmten Typs geschahen ?, , gefiltert durch eine Tonne Parameter Liek country . Umami API funktioniert nur über JavaScriptSo wollend, mit diesen Daten zu spielen Ich ging durch die Mühe der Schaffung eines Clients für es C.#...

Das UmamiDataService class ist bis in multple Teilklassen unterteilt. Die Methoden sind SUPER lang. PageViews Methode:

Sie können sehen, dass MUCH des Codes die QueryString aus der übergebenen in PageViewsRequest class baut.Es gibt andere Möglichkeiten, dies zu tun, aber dies...,... zum Beispiel mit Attributen oder Reflexion arbeitet hier...

GetPageViews
    public async Task<UmamiResult<PageViewsResponseModel>> GetPageViews(PageViewsRequest pageViewsRequest)
    {
        if (await authService.LoginAsync() == false)
            return new UmamiResult<PageViewsResponseModel>(HttpStatusCode.Unauthorized, "Failed to login", null);
        // Start building the query string
        var queryParams = new List<string>
        {
            $"startAt={pageViewsRequest.StartAt}",
            $"endAt={pageViewsRequest.EndAt}",
            $"unit={pageViewsRequest.Unit.ToLowerString()}"
        };

        // Add optional parameters if they are not null
        if (!string.IsNullOrEmpty(pageViewsRequest.Timezone)) queryParams.Add($"timezone={pageViewsRequest.Timezone}");
        if (!string.IsNullOrEmpty(pageViewsRequest.Url)) queryParams.Add($"url={pageViewsRequest.Url}");
        if (!string.IsNullOrEmpty(pageViewsRequest.Referrer)) queryParams.Add($"referrer={pageViewsRequest.Referrer}");
        if (!string.IsNullOrEmpty(pageViewsRequest.Title)) queryParams.Add($"title={pageViewsRequest.Title}");
        if (!string.IsNullOrEmpty(pageViewsRequest.Host)) queryParams.Add($"host={pageViewsRequest.Host}");
        if (!string.IsNullOrEmpty(pageViewsRequest.Os)) queryParams.Add($"os={pageViewsRequest.Os}");
        if (!string.IsNullOrEmpty(pageViewsRequest.Browser)) queryParams.Add($"browser={pageViewsRequest.Browser}");
        if (!string.IsNullOrEmpty(pageViewsRequest.Device)) queryParams.Add($"device={pageViewsRequest.Device}");
        if (!string.IsNullOrEmpty(pageViewsRequest.Country)) queryParams.Add($"country={pageViewsRequest.Country}");
        if (!string.IsNullOrEmpty(pageViewsRequest.Region)) queryParams.Add($"region={pageViewsRequest.Region}");
        if (!string.IsNullOrEmpty(pageViewsRequest.City)) queryParams.Add($"city={pageViewsRequest.City}");

        // Combine the query parameters into a query string
        var queryString = string.Join("&", queryParams);

        // Make the HTTP request
        var response = await authService.HttpClient.GetAsync($"/api/websites/{WebsiteId}/pageviews?{queryString}");

        if (response.IsSuccessStatusCode)
        {
            logger.LogInformation("Successfully got page views");
            var content = await response.Content.ReadFromJsonAsync<PageViewsResponseModel>();
            return new UmamiResult<PageViewsResponseModel>(response.StatusCode, response.ReasonPhrase ?? "Success",
                content ?? new PageViewsResponseModel());
        }

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            await authService.LoginAsync();
            return await GetPageViews(pageViewsRequest);
        }

        logger.LogError("Failed to get page views");
        return new UmamiResult<PageViewsResponseModel>(response.StatusCode,
            response.ReasonPhrase ?? "Failed to get page views", null);
    }
Wie Sie sehen können, baut dies wirklich nur eine Abfrage string-.. , authentifiziert den Aufruf [letzter Artikel](/blog/unittestinglogginginaspnetcore) für einige Details auf diesem und macht dann den Anruf an die Umami API, also wie testen wir diese

Testen des UmamiDataService

Im Gegensatz zum Testen von UmamiClient-, testete ich die UmamiDataService ohne die Verwendung von Moq-., stattdessen, habe ich eine einfache DelegatingHandler Klasse, die es mir erlaubt, die Anfrage zu verhören und dann eine Antwort zurückgeben Dies ist ein viel einfacherer Ansatz als mit Moq und ermöglicht es mir zu testen, die UmamiDataService ohne zu verspotten die HttpClient.

Im Code unten sehen Sie, dass ich einfach expandiere DelegatingHandler und überschreiben SendAsync Methode: Diese Methode erlaubt es mir, die Anfrage zu prüfen und eine Antwort basierend auf der Anfrage zurückzugeben.

public class UmamiDataDelegatingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var absPath = request.RequestUri.AbsolutePath;
        switch (absPath)
        {
            case "/api/auth/login":
                var authContent = await request.Content.ReadFromJsonAsync<AuthRequest>(cancellationToken);
                if (authContent?.username == "username" && authContent?.password == "password")
                    return ReturnAuthenticatedMessage();
                else if (authContent?.username == "bad")
                {
                    return new HttpResponseMessage(HttpStatusCode.Unauthorized);
                }
                else
                {
                    return new HttpResponseMessage(HttpStatusCode.BadRequest);
                }
            default:

                if (absPath.StartsWith($"/api/websites/{Consts.WebSiteId}/pageviews"))
                {
                    var pageViews = GetParams<PageViewsRequest> (request);
                  
                    return ReturnPageViewsMessage(pageViews);
                }

                if (absPath.StartsWith($"/api/websites/{Consts.WebSiteId}/metrics"))
                {
                    var metricsRequest = GetParams<MetricsRequest>(request);
                    return ReturnMetrics(metricsRequest);
                }

                return new HttpResponseMessage(HttpStatusCode.NotFound);
        }
    }
 }

Einrichtung

Um die neue UmamiDataService um diesen Handler zu verwenden ist ähnlich einfach

    public IServiceProvider GetServiceProvider (string username="username", string password="password")
    {
        var services = new ServiceCollection();
        var mockLogger = new FakeLogger<UmamiDataService>();
        var authLogger = new FakeLogger<AuthService>();
        services.AddScoped<ILogger<UmamiDataService>>(_ => mockLogger);
        services.AddScoped<ILogger<AuthService>>(_ => authLogger);
        services.SetupUmamiData(username, password);
        return  services.BuildServiceProvider();
        
    }

Sie sehen, dass ich gerade die ServiceCollectionEinfügen der FakeLogger<T> Siehe auch: letzter Artikel für Details zu diesem und dann die UmamiData service mit dem Benutzernamen und Passwort Ich möchte verwenden, um (.so kann ich Testfehler.)..

Ich rufe dann in services.SetupUmamiData(username, password); das ist eine Erweiterungsmethode, die ich erstellt habe, um die UmamiDataService mit der UmamiDataDelegatingHandler und der AuthService;

    public static void SetupUmamiData(this IServiceCollection services, string username="username", string password="password")
    {
        var umamiSettings = new UmamiDataSettings()
        {
            UmamiPath = Consts.UmamiPath,
            Username = username,
            Password = password,
            WebsiteId = Consts.WebSiteId
        };
        services.AddSingleton(umamiSettings);
        services.AddHttpClient<AuthService>((provider,client) =>
        {
            client.BaseAddress = new Uri(umamiSettings.UmamiPath);
            

        }).AddHttpMessageHandler<UmamiDataDelegatingHandler>()
            .SetHandlerLifetime(TimeSpan.FromMinutes(5));  //Set lifetime to five minutes

        services.AddScoped<UmamiDataDelegatingHandler>();
        services.AddScoped<UmamiDataService>();
    }

Sie können sehen, dass dies ist, wo ich Haken in der UmamiDataDelegatingHandler und der AuthService zu dem UmamiDataServiceDie Art und Weise, wie dies ist strukturiert ist, dass die AuthService ',owns ,', HttpClient und der UmamiDataService verwendet die AuthService um die Anrufe zur Umami API mit dem bearer Zeichen und BaseAddress bereits eingestellt

Die Prüfungen

Wirklich macht dies tatsächlich das Testen dieses wirklich einfach ,. , es ist nur ein bisschen verbal, da ich auch die Protokollierung zu testen wollte , . , Alles was es tun, ist durch meine Posting DelegatingHandler und ich simulieren eine Antwort basierend auf der Anfrage

public class UmamiData_PageViewsRequest_Test : UmamiDataBase
{
    private readonly DateTime StartDate = DateTime.ParseExact("2021-10-01", "yyyy-MM-dd", null);
    private readonly DateTime EndDate = DateTime.ParseExact("2021-10-07", "yyyy-MM-dd", null);
    
    [Fact]
    public async Task SetupTest_Good()
    {
        var serviceProvider = GetServiceProvider();
        var umamiDataService = serviceProvider.GetRequiredService<UmamiDataService>();
        var authLogger = serviceProvider.GetRequiredService<ILogger<AuthService>>();
        var umamiDataLogger = serviceProvider.GetRequiredService<ILogger<UmamiDataService>>();
        var result = await umamiDataService.GetPageViews(StartDate, EndDate);
        var fakeAuthLogger = (FakeLogger<AuthService>)authLogger;
        FakeLogCollector collector = fakeAuthLogger.Collector; 
        IReadOnlyList<FakeLogRecord> logs = collector.GetSnapshot();
        Assert.Contains("Login successful", logs.Select(x => x.Message));
        
        var fakeUmamiDataLogger = (FakeLogger<UmamiDataService>)umamiDataLogger;
        FakeLogCollector umamiDataCollector = fakeUmamiDataLogger.Collector;
        IReadOnlyList<FakeLogRecord> umamiDataLogs = umamiDataCollector.GetSnapshot();
        Assert.Contains("Successfully got page views", umamiDataLogs.Select(x => x.Message));
        
        Assert.NotNull(result);
    }
}

Simulation der Reaktion

Um die Antwort für diese Methode zu simulieren Sie sich an mich erinnern, habe ich diese Zeile in der UmamiDataDelegatingHandler:

  if (absPath.StartsWith($"/api/websites/{Consts.WebSiteId}/pageviews"))
                {
                    var pageViews = GetParams<PageViewsRequest> (request);
                  
                    return ReturnPageViewsMessage(pageViews);
                }

Alles, was dies tut, ist Informationen aus dem Querystring zu ziehen und konstruiert eine sehr kleine docs auf diesem I)..You.'.ll sehen Ich teste für die Anzahl der Tage zwischen Start- und Enddatum und dann geben Sie eine Antwort mit der gleichen Anzahl von Tagen.

    private static HttpResponseMessage ReturnPageViewsMessage(PageViewsRequest request)
    {
        var startAt = request.StartAt;
        var endAt = request.EndAt;
        var startDate = DateTimeOffset.FromUnixTimeMilliseconds(startAt).DateTime;
        var endDate = DateTimeOffset.FromUnixTimeMilliseconds(endAt).DateTime;
        var days = (endDate - startDate).Days;

        var pageViewsList = new List<PageViewsResponseModel.Pageviews>();
        var sessionsList = new List<PageViewsResponseModel.Sessions>();
        for(int i=0; i<days; i++)
        {
            
            pageViewsList.Add(new PageViewsResponseModel.Pageviews()
            {
                x = startDate.AddDays(i).ToString("yyyy-MM-dd"),
                y = i*4
            });
            sessionsList.Add(new PageViewsResponseModel.Sessions()
            {
                x = startDate.AddDays(i).ToString("yyyy-MM-dd"),
                y = i*8
            });
        }
        var pageViewResponse = new PageViewsResponseModel()
        {
            pageviews = pageViewsList.ToArray(),
            sessions = sessionsList.ToArray()
        };
        var json = JsonSerializer.Serialize(pageViewResponse);
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(json, Encoding.UTF8, "application/json")
        };
    }

Schlussfolgerung

So dass es wirklich es ist, ' ziemlich einfach zu testen ein HttpClient Anfrage ohne Verwendung von Moq und ich denke, es ist weit sauberer auf diese Weise... Sie verlieren einige der Raffinesse in Moq möglich gemacht aber für einfache Tests wie diese...

Finding related posts...
logo

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