Eenheidstest Umami.Net -Testen van Umami Data zonder Moq te gebruiken (Nederlands (Dutch))

Eenheidstest Umami.Net -Testen van Umami Data zonder Moq te gebruiken

Wednesday, 04 September 2024

//

8 minute read

Inleiding

In het vorige deel van deze reeks heb ik getest. Umami.Net-trackingmethoden

Het probleem

In het vorige deel gebruikte ik Moq om me een Mock<HttpMessageHandler> en terugvoer de handvater die gebruikt is in UmamiClient, dit is een gemeenschappelijk patroon bij het testen van code dat gebruik maakt van HttpClient. In deze post zal ik je laten zien hoe je de nieuwe test moet testen. UmamiDataService zonder Moq. te gebruiken

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

Waarom Moq? gebruiken

Moq is een krachtige mocking-bibliotheek waarmee je objecten kunt maken voor interfaces en klasse.. Het wordt algemeen gebruikt in eenheidstests om de code te isoleren van haar afhangingen.

Het voorbeeld dat ik eerder gaf, geeft veel flexibiliteit in het testen van de UmamiClient class, maar het heeft ook een paar drawbacks. Het is UGLY code en doet een heleboel dingen die ik niet nodig heb. UmamiDataService Ik besloot een andere aanpak te proberen.

Testen van UmamiDataService

De gemeenschappelijke wetgeving van het land. UmamiDataService is een toekomstige toevoeging aan de Umami.Net-bibliotheek waarmee je gegevens van Umami kan halen om dingen te zien zoals hoeveel aansigten er op een pagina waren. Umami API werkt alleen via JavaScript.. Ik wilde dus met die gegevens spelen en ging er een C-client voor maken.

De gemeenschappelijke wetgeving van het land. UmamiDataService class is verdeeld in veelvuldige gedeeltelijke klasse (de methodes zijn SUPER lang) bijvoorbeeld hierM SK2s de PageViews methode.

Je kunt zien dat MUCH van de code de QueryString bouwt uit het doorgegeven in PageViewsRequest klas (er zijn andere manieren om dit te doen maar deze, bijvoorbeeld met Attributen of reflectiewerk hierM SK2

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);
    }
Zoals je kunt zien bouwt dit gewoon een zoek String . de roep authentifieert. [laatste artikel](/blog/unittestinglogginginaspnetcore) voor een paar details over dit, en dan roept hij naar de Umami API.

De UmamiDataService testen

In tegenstelling tot het testen van UmamiClient, besloot ik de UmamiDataService zonder Moq. Ik creëerde een eenvoudige DelegatingHandler Dit is een veel eenvoudiger aanpak dan Moq gebruiken en ik kan de vraag testen. UmamiDataService Ik hoefde het niet te bespoten. HttpClient.

In de code onder kan je zien dat ik eenvoudig uitbreng. DelegatingHandler en overskryf de SendAsync methode. Deze methode laat me toe om de vraag te controleren en een antwoord terug te geven gebaseerd op de vraag.

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

Opstelling

Om de nieuwe op te zetten. UmamiDataService om deze handvater te gebruiken is vergelijkbaar eenvoudig.

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

Je zal zien dat ik net een computer heb opgezet. ServiceCollection, voeg de FakeLogger<T> ( Kijk opnieuw naar de laatste artikel voor details hierover. En dan zetten we de computers op. UmamiData service met de gebruikernaam en wachtwoord die ik wil gebruiken.

Dan bel ik in. services.SetupUmamiData(username, password); Dit is een uitbreidingsmethode die ik creëerde om de UmamiDataService met de UmamiDataDelegatingHandler en de 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>();
    }

Je kunt zien dat dit is waar ik in de UmamiDataDelegatingHandler en de AuthService naar de UmamiDataService. De manier waarop dit is gestructureerd, is dat de AuthService 'eigent' de HttpClient en de UmamiDataService het gebruik van de AuthService om aan de Umami API te bellen. bearer Token en BaseAddress al gezet.

De tests

Dit maakt het testen echt heel eenvoudig. DelegatingHandler Ik simuleer een reactie gebaseerd op de vraag..

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

Simulatie van de reactie

Om de reactie voor deze methode te simuleren, herinner je je aan dat ik deze lijn in de UmamiDataDelegatingHandler:

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

Dit haalt alleen informatie uit de zoekstring en bouwt een 'realistische' antwoord op basis van Live Tests. Ik heb 'omge compileerdM SK4 nogmaals heel weinig docs op dit.MSC5 Jullie zullen zien dat ik test voor het aantal dagen tussen de start- en einddatum en dan een antwoord terugvoer met hetzelfde aantal dagen.

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

In slotting:

Het is dus heel eenvoudig om een test te doen. HttpClient request without using Moq and I think it's far cleaner this way. You DO lose some of the sophistication made possible in Moq but for simple tests like this

Finding related posts...
logo

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