Back to "Enhetstest av Umami.Net - Test av UmamiClient"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Umami xUnit

Enhetstest av Umami.Net - Test av UmamiClient

Friday, 13 September 2024

Inledning

Nu har jag Ummami.Net-paket Där ute vill jag naturligtvis se till att allt fungerar som förväntat. För att göra detta är det bästa sättet att något omfattande testa alla metoder och klasser. Det är här enhetstester kommer in. Observera: Detta är inte en "perfekt metod" typ inlägg, det är bara hur jag för närvarande har gjort det. I själva verket behöver jag inte verkligen för att Mock IHttpMessageHandler Här en du kan attackera en DelegatingMessageHandler till en normal HttpClient för att göra detta. Jag ville bara visa hur du kan göra det med en Mock.

Enhetstest

Enhetstester avser processen för att testa enskilda kodenheter för att säkerställa att de fungerar som förväntat. Detta görs genom att skriva tester som kallar metoderna och klasserna på ett kontrollerat sätt och sedan kontrollera utdata är som förväntat.

För ett paket som Umami.Net detta är soewhat knepigt som det båda kallar en fjärrklient över HttpClient och har en IHostedService Det används för att göra sändandet av nya händelsedata så sömlöst som möjligt.

Pröva UmamiClient

Huvuddelen av testerna HttpClient baserat bibliotek undviker det faktiska "HttpClient"-samtalet. Detta görs genom att skapa en HttpClient som använder en HttpMessageHandler som ger ett känt svar. Detta görs genom att skapa en HttpClient med en HttpMessageHandler som returnerar ett känt svar; i detta fall jag bara ekor tillbaka inmatningssvaret och kontrollera som inte har manglats av UmamiClient.

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

Som du kommer att se detta sätter upp en Mock<HttpMessageHandler> Jag passerar sedan in i UmamiClient. I den här koden kopplar jag in den här i vår IServiceCollection Uppställningsmetod. Detta lägger till alla tjänster som krävs av UmamiClient inklusive vår nya HttpMessageHandler och sedan returnerar IServiceCollection för användning i testerna.

    public static IServiceCollection SetupServiceCollection(string webSiteId = Consts.WebSiteId,
        string umamiPath = Consts.UmamiPath, HttpMessageHandler? handler = null)
    {
        var services = new ServiceCollection();
        var umamiClientSettings = new UmamiClientSettings
        {
            WebsiteId = webSiteId,
            UmamiPath = umamiPath
        };
        services.AddSingleton(umamiClientSettings);
        services.AddScoped<PayloadService>();
        services.AddLogging(x => x.AddConsole());
        // Mocking HttpMessageHandler with Moq
        var mockHandler = handler ?? EchoMockHandler.Create();
        services.AddHttpClient<UmamiClient>((serviceProvider, client) =>
        {
            var umamiSettings = serviceProvider.GetRequiredService<UmamiClientSettings>();
            client.BaseAddress = new Uri(umamiSettings.UmamiPath);
        }).ConfigurePrimaryHttpMessageHandler(() => mockHandler);
        return services;
    }

För att använda detta och injicera det i UmamiClient Jag använder sedan dessa tjänster i UmamiClient Uppställning.

    public static UmamiClient GetUmamiClient(IServiceCollection? serviceCollection = null,
        HttpContextAccessor? contextAccessor = null)
    {
        serviceCollection ??= SetupServiceCollection();
        SetupUmamiClient(serviceCollection, contextAccessor);
        if (serviceCollection == null) throw new NullReferenceException(nameof(serviceCollection));
        var serviceProvider = serviceCollection.BuildServiceProvider();
        return serviceProvider.GetRequiredService<UmamiClient>();
    }

Du ser att jag har ett gäng alternativa parametrar här som tillåter mig att injicera olika alternativ för olika typer av tester.

Testerna

Så nu har jag all denna installation på plats jag kan nu börja skriva tester för UmamiClient Metoder.

Skicka

Vad allt detta inställning innebär är att våra tester faktiskt kan vara ganska enkel

public class UmamiClient_SendTests
{
    [Fact]
    public async Task Send_Wrong_Type()
    {
        var umamiClient = SetupExtensions.GetUmamiClient();
        await Assert.ThrowsAsync<ArgumentException>(async () => await umamiClient.Send(type: "boop"));
    }

    [Fact]
    public async Task Send_Empty_Success()
    {
        var umamiClient = SetupExtensions.GetUmamiClient();
        var response = await umamiClient.Send();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}

Här ser du den enklaste test fall, bara se till att UmamiClient kan skicka ett meddelande och få ett svar; viktigt är att vi också testar för ett undantag fall där type Det är fel. Detta är en ofta förbisedd del av testning, se till att koden misslyckas som förväntat.

Sidvy

För att testa vår pageview metod kan vi göra något liknande. I koden nedan använder jag min EchoHttpHandler Att bara reflektera tillbaka det skickade svaret och se till att det skickar tillbaka det jag förväntar mig.

    [Fact]
    public async Task TrackPageView_WithNoUrl()
    {
        var defaultUrl = "/testpath";
        var contextAccessor = SetupExtensions.SetupHttpContextAccessor(path: "/testpath");
        var umamiClient = SetupExtensions.GetUmamiClient(contextAccessor: contextAccessor);
        var response = await umamiClient.TrackPageView();

        var content = await response.Content.ReadFromJsonAsync<EchoedRequest>();
        Assert.NotNull(response);
        Assert.NotNull(content);
        Assert.Equal(content.Payload.Url, defaultUrl);
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }

HttpContextAccessor

Detta använder sig av HttpContextAccessor för att ställa in sökvägen till /testpath och sedan kontrollera att UmamiClient skickar detta på rätt sätt.

    public static HttpContextAccessor SetupHttpContextAccessor(string host = Consts.Host,
        string path = Consts.Path, string ip = Consts.Ip, string userAgent = Consts.UserAgent,
        string referer = Consts.Referer)
    {
        HttpContext httpContext = new DefaultHttpContext();
        httpContext.Request.Host = new HostString(host);
        httpContext.Request.Path = new PathString(path);
        httpContext.Connection.RemoteIpAddress = IPAddress.Parse(ip);
        httpContext.Request.Headers.UserAgent = userAgent;
        httpContext.Request.Headers.Referer = referer;

        var context = new HttpContextAccessor { HttpContext = httpContext };
        return context;
    }

Detta är viktigt för vår Umami klient kod eftersom mycket av de data som skickas från varje begäran faktiskt dynamiskt genereras från HttpContext motsätter sig detta. Så vi kan inte skicka något alls i en await umamiClient.TrackPageView(); ring och det kommer fortfarande att skicka rätt data genom att extrahera Url från HttpContext.

Som vi kommer att se senare är det också viktigt awe skicka objekt som UserAgent och IPAddress eftersom dessa används av Umami-servern för att spåra data och'spåra' användarvyer utan att använda cookies.

För att ha denna förutsägbara vi definierar ett gäng av Constants i Consts Klassen. Så vi kan testa mot förutsägbara svar och förfrågningar.

public class Consts
{
    public const string UmamiPath = "https://example.com";
    public const string WebSiteId = "B41A9964-FD33-4108-B6EC-9A6B68150763";
    public const string Host = "example.com";
    public const string Path = "/example";
    public const string Ip = "127.0.0.1";
    public const string UserAgent = "Test User Agent";
    public const string Referer = "Test Referer";
    public const string DefaultUrl = "/testpath";
    public const string DefaultTitle = "Example Page";
    public const string DefaultName = "RSS";
    public const string DefaultType = "event";

    public const string Email = "[email protected]";

    public const string UserId = "11224456";
    
    public const string UserName = "Test User";
    
    public const string SessionId = "B41A9964-FD33-4108-B6EC-9A6B68150763";
}

Ytterligare tester

Detta är bara början på vår teststrategi för Umami.Net, vi måste fortfarande testa IHostedService och testa mot de faktiska data Umami genererar (som inte dokumenteras någonstans men innehåller en JWT token med några användbara data.)

{
  "alg": "HS256",
  "typ": "JWT"
}{
  "id": "b9836672-feee-55c5-985a-a5a23d4a23ad",
  "websiteId": "32c2aa31-b1ac-44c0-b8f3-ff1f50403bee",
  "hostname": "example.com",
  "browser": "chrome",
  "os": "Windows 10",
  "device": "desktop",
  "screen": "1920x1080",
  "language": "en-US",
  "country": "GB",
  "subdivision1": null,
  "subdivision2": null,
  "city": null,
  "createdAt": "2024-09-01T09:26:14.418Z",
  "visitId": "e7a6542f-671a-5573-ab32-45244474da47",
  "iat": 1725182817
}2|Y*: �(N%-ޘ^1>@V

Så vi kommer att vilja testa för det, simulera token och eventuellt returnera data vid varje besök (som du kommer ihåg detta är gjord av en uuid(websiteId,ipaddress, useragent)).

Slutsatser

Detta är bara början på att testa Umami.Net paketet, det finns mycket mer att göra men detta är en bra början. Jag kommer att lägga till fler tester när jag går och utan tvekan förbättra dessa.

logo

©2024 Scott Galloway