Back to "Unité d'essai Umami.Net - Essais de données Umami sans Moq"

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

ASP.NET Core xUnit

Unité d'essai Umami.Net - Essais de données Umami sans Moq

Wednesday, 04 September 2024

Introduction

Dans la partie précédente de cette série où j'ai testé Umami.Methodes de suivi du réseau

Le problème

Dans la partie précédente, j'ai utilisé Moq pour me donner un Mock<HttpMessageHandler> et retourner le gestionnaire utilisé dans UmamiClient, il s’agit d’un modèle commun lors de l’essai du code qui utilise HttpClient. Dans ce post je vous montrerai comment tester le nouveau UmamiDataService sans utiliser 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;
    }

Pourquoi utiliser Moq?

Moq est une puissante bibliothèque de montage qui vous permet de créer des objets de montage pour les interfaces et les classes.. Il est largement utilisé dans l'essai d'unités afin d'isoler le code soumis à un essai de ses dépendances.

L'exemple que j'ai donné ci-dessus donne beaucoup de souplesse pour tester les UmamiClient class, mais il a aussi quelques inconvénients . Il' est un code UGLY et fait beaucoup de choses que je n’ai pas besoin. UmamiDataService J’ai décidé d’essayer une approche différente.

Essai du service UmamiDataService

L'article 5 de la Loi sur l'immigration et le statut des réfugiés UmamiDataService est un futur ajout à la bibliothèque Umami.Net qui vous permettra de récupérer des données d'Umami pour des choses comme voir combien de vues une page a eu , quels sont les événements survenus d'un certain type, filtré par une tonne de paramètres liek paysMSC3 villeM SK4 OSMska5 taille de l'écranM Ska6 etcMSKA7 Il s'agit d'une très puissante mais en ce moment, Umami API ne fonctionne que par JavaScript. Alors, désirant jouer avec ces données, j’ai déployé des efforts pour y créer un client C#

L'article 5 de la Loi sur l'immigration et le statut des réfugiés UmamiDataService La classe est divisée en classes partielles multiples (les méthodes sont SUPER longuesM SK1 par exemple ici's la PageViews méthode.

Vous pouvez voir que MUCH du code construit la QueryString à partir de ce qui a été passé dans la classe PageViewsRequest (il y a d’autres façons de le faire mais c’est cette , par exemple en utilisant les Attributes ou les travaux de réflexion ici).

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);
    }
Comme vous pouvez le voir, ce n'est que la construction d'une chaîne de demande. authentifie l'appel (voir [dernier article](/blog/unittestinglogginginaspnetcore) pour obtenir des détails sur ce ) et ensuite faire le appel à l'API Umami . Alors comment tester ce

Essai du service UmamiDataService

En contraste avec le test UmamiClient,, j’ai décidé de tester UmamiDataService sans utiliser Moq. Au lieu de, j’ai créé un simple DelegatingHandler class qui m’autorise à interroger la demande puis à retourner une réponse. Cette approche est beaucoup plus simple que l’utilisation de Moq et me permet d’essayer UmamiDataService sans avoir à reprocher le HttpClient.

Dans le code ci-dessous vous pouvez voir que je prolonge simplement DelegatingHandler et supprimer le SendAsync method. Cette méthode me permet d'inspecter la demande et de lui retourner une réponse fondée sur celle-ci.

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

Mise en place

Pour mettre en place le nouveau UmamiDataService pour utiliser ce gestionnaire est similairement simple.

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

Vous verrez que je viens de mettre en place le ServiceCollection, ajouter le FakeLogger<T> ( voir de nouveau le dernier article pour plus de détails sur ce sujet et ensuite mettre en place le UmamiData service avec le nom d'utilisateur et le mot de passe que je veux utiliser (sois-ce que je peux tester l'échec

J'appelle ensuite services.SetupUmamiData(username, password); qui est une méthode d'extension que j'ai créée pour mettre en place le UmamiDataService avec le UmamiDataDelegatingHandler et le 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>();
    }

Vous pouvez voir que c'est là où je me connecte à la UmamiDataDelegatingHandler et le AuthService à la UmamiDataService. La structure de ce système est la suivante : AuthService 'owns' the HttpClient et le UmamiDataService utilise le AuthService pour faire les appels à l'API Umami avec le bearer d'un symbole et BaseAddress déjà établie.

Les essais

C’est vraiment très simple à tester. DelegatingHandler et je simule une réponse basée sur la demande.

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 de la réponse

Pour simuler la réponse de cette méthode, vous se rappelez que j’ai cette ligne dans UmamiDataDelegatingHandler:

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

Tout ce qui se fait est de retirer l'information de la chaîne de demande et de construire une réponse 'réaliste ' basée sur des essais en direct IM SK3ai compilé, de nouveau très peu d'articles sur cette questionMNK5 VousMRK6vous verrez que j'essaie le nombre de jours entre la date de début et celle de fin, puis je retourne une réponse avec le même nombre de journéesMMK7

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

En conclusion

Ainsi, il est très simple de tester un HttpClient demande sans utiliser Moq et je crois que c’est beaucoup plus propre de cette façon. . Vous perdez une certaine sophistication rendue possible par Moq, mais pour des tests simples comme celui-ci.

logo

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