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

# Introduction

Dans la partie précédente de cette série où j'ai testé[ Umami.Methodes de suivi du réseau ](/blog/unittestingumaminet)

<!--category-- xUnit, ASP.NET Core -->
<datetime class="hidden">2024-09-04T20:30</datetime>
[TOC]

## 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.

```csharp
    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](https://umami.is/docs/api/website-stats). 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).

<details>
<summary>GetPageViews</summary>

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

</details>
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.

```csharp
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.

```csharp
    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](/blog/unittestinglogginginaspnetcore) 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`;

```csharp
    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.

```csharp
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`:

```csharp
  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

```csharp
    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.