Unit Testing Umami.Net M SK1 Umami-tietojen testaaminen Moq:n käyttämättä (Suomi (Finnish))

Unit Testing Umami.Net M SK1 Umami-tietojen testaaminen Moq:n käyttämättä

Wednesday, 04 September 2024

//

7 minute read

Esitys

Edellinen osa tästä sarjasta, jossa testasin Umami.Nettiseurantamenetelmät

Ongelma

Edellisen osan aikana käytin Moq:ta antaakseni Mock<HttpMessageHandler> ja palauttaa käsittelijä, jota käytetään UmamiClient, tämä on yleinen malli testattaessa koodia, jossa käytetään HttpClient. Tässä artikkelissa kerron teille, miten testata uusi UmamiDataService käyttämättä 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;
    }

Miksi Moq?

Moq on tehokas mocking-libraari, jonka avulla voidaan luoda malliesineitä liittymiin ja luokkiin.. Sitä käytetään laajasti yksiköiden testauksessa eristääkseen testattavan kodin sen riippuvuussuhteista.

Edellä mainitsemani esimerkki antaa paljon joustavuutta testattaessa UmamiClient luokka, mutta sillä on myös joitakin haittoja . SeM SK2s UGLY-koodi ja tekee monenlaisia asioita, joita en tarvitse UmamiDataService Päätin yrittää toisenlaista lähestymistapaa.

UmamiDataServicen testaaminen

Euroopan unionin UmamiDataService on tuleva lisä Umamiin.Net-libraariin, joka antaa mahdollisuuden hakea tietoja Umamista asioihin kuten siihen, kuinka monta näkökohtaa sivulla oli Umami API toimii vain JavaScriptin kautta. Halusin siis leikkiä kyseisten tietojen kanssa ja pyrin luomaan sille C-clientin.

Euroopan unionin UmamiDataService luokka on jaettu moninkertaisiin osittaisiin luokkiin (menetelmät ovat SUPER pitkiäM SK1 esim. PageViews menetelmä.

Voitte nähdä, että suurin osa koodista rakentaa QueryString-aluetta PageViewsRequest -luokan välityksellä.

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);
    }
Kuten näette, tämä todellakin vain rakentaa kysymyksen stringin. todistaa puheenvuoron (katso [viimeinen artikkeli](/blog/unittestinglogginginaspnetcore) joitakin yksityiskohtia tästä) ja sen jälkeen antaa puheenvuoron Umami API:lle. Miten testaamme tämänM SK2

UmamiDataServicen testaaminen

Toisin kuin UmamiClient, -testissä, päätin testata UmamiDataService muttei käyttämällä Moq. Sen sijaan, luon yksinkertaisen DelegatingHandler luokka, joka antaa minulle mahdollisuuden tutkia pyyntöä ja palauttaa sen jälkeen vastauksen. Tämä on paljon yksinkertaisempi lähestymistapa kuin Moqin käyttö ja se antaa minulle tilaisuuden testata UmamiDataService ilman, että meidän on moittittava HttpClient.

jäljempänä olevassa koodissa voitte nähdä, että laajenn DelegatingHandler ja poistaa SendAsync menetelmä. Tällä menetelmällä voin tarkastaa pyynnön ja palauttaa sen perusteella vastauksen

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

Järjestelmä

Uuden järjestelmän käyttöönotto UmamiDataService tämän käsittelijän käyttäminen on yhtä yksinkertaista.

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

Näette, että olen juuri perustanut ServiceCollection, lisätään FakeLogger<T> (katso uudelleen Viimeinen artikkeli yksityiskohdista ja sen jälkeen perustaa UmamiData palvelu, johon sisältyy käyttäjänimi ja -tunnus, jota haluan käyttää (niten voin testata epäonnistumisen

Vetoan sitten services.SetupUmamiData(username, password); joka on laajentumismenetelmä, jonka olen luonut UmamiDataService neuvoston UmamiDataDelegatingHandler ja 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>();
    }

Voitte nähdä, että tämä on se, missä kiinnitän UmamiDataDelegatingHandler ja AuthService neuvoston UmamiDataService. Järjestelmä on sellainen, että AuthService ' HttpClient ja UmamiDataService käyttää AuthService Umami API:hun tehtävien puhelujen tekemiseen bearer merkki ja BaseAddress jo asetettu.

Testit

Todellisuudessa tämä tekee itse asiassa testauksesta tämän todella yksinkertaisen. Se' on vain hieman kielellinen, koska minäkin halusin testata merkintää myösMSC2 Kaikki seM SK3s tekeminen on postittaminen minun DelegatingHandler ja simuloin vastauksen pyynnöstä.

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

Vastauksen simulointi

Toistaakseen tämän menetelmän vastauksen muistatte, että minulla on tämä rivi UmamiDataDelegatingHandler:

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

Kaikki tämä tekee on vetää tietoa kysymyksen stringista ja rakentaa 'realistinen' vastaus (perustana Live-testeihin IM SK3ve compiledMSC4 jälleen hyvin vähän asiakirjoja tästäMST5 TeidänMSV6katsotte, että testaan päivämäärää lähtö- ja loppupäivämäärän välillä ja sitten palaan vastauksen saman päivämäärän kanssa

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

Päätelmissä

Näin ollen on todellakin hyvin yksinkertaista testata HttpClient pyyntöä käyttämättä Moq-ohjelmaa, ja uskon sen olevan parempi.' on tällä tavoin paljon puhtaampi.

Finding related posts...
logo

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