# Unità di test Umami.Net - Test dei dati Umami senza usare Moq

# Introduzione

Nella parte precedente di questa serie dove ho testato[ Umami.Methodi di tracciamento del network ](/blog/unittestingumaminet)

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

## Il problema

Nella parte precedente ho usato Moq per darmi un'idea. `Mock<HttpMessageHandler>` e restituire il handler usato in `UmamiClient`, questo è un modello comune quando si testa il codice che usa `HttpClient`. In questo post vi mostrerò come testare il nuovo. `UmamiDataService` senza usare 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;
    }
```

## Perché usare Moq?

Moq è una potente biblioteca di mocking che ti permette di creare oggetti da modellare per interfacce e classi. È largamente usata nell'unità di test per isolare il codice sotto controllo dalle sue dipendenze. TuttaviaM SK2 ci sono alcuni casi in cui l'uso di Moq può essere pesante o persino impossibile . Per esempioMSC4 quando viene testato un codice che usa metodi statici o quando il codice subposto è stretto agganciato alle sue dipendenza

L'esempio che vi ho dato ci dà molta flessibilità per testare. `UmamiClient` class, ma ha anche alcuni difetti. Ha il UGLY code di ' e fa un sacco di cose che non ne ho bisogno. `UmamiDataService` Ho deciso di provare un approccio diverso.

# Testare UmamiDataService

L'informazione figura nella parte dispositiva. `UmamiDataService` è un'addizione futura alla biblioteca Umami.Net che vi permetterà di raccogliere dati da Umami per cose come vedere quante visuali aveva una pagina, quali eventi si sono verificati di un certo tipoM SK2 filtrati da una tonnellata di parametri liek country , cittàMST4 OSM ST5 dimensione dello schermoMst6 eccM st7 Questo è molto potente ma proprio ora la [Umami API funziona solo attraverso JavaScript.](https://umami.is/docs/api/website-stats). Quindi volevo giocare con quei dati e ho cercato di creare un cliente per lui.

L'informazione figura nella parte dispositiva. `UmamiDataService` La classe è divisa in classi partiali multiple (le metodi sono SUPER lungheM SK1 per esempio qui's la `PageViews` metodo.

Potete vedere che MUCH del codice sta costruendo la QueryString dal passato nella classe PageViewsRequest (ci sono altri modi per farlo ma questo, per esempio usando Attribute o il lavoro di riflesso quiM SK2

<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>
Come potete vedere questo semplicemente costruisce una sequenza di query. authentica l'appello ( vediamo il [ultimo articolo](/blog/unittestinglogginginaspnetcore) per qualche dettaglio su questo ) e poi fa la chiamata all'API di Umami.

## Testare il UmamiDataService

Contrariamente al test UmamiClient, ho deciso di testare `UmamiDataService` senza usare Moq. Invece, Ho creato un semplice `DelegatingHandler` Classe che mi permette di interrogare la richiesta e poi restituire una risposta. Questo è un approccio molto più semplice rispetto all'uso del Moq e mi consente di testare il `UmamiDataService` senza dover ridicolo. `HttpClient`.

Nel codice sotto potete vedere che semplicemente estendo `DelegatingHandler` e superare il `SendAsync` metodo. Questo metodo mi permette di esaminare la richiesta e restituire una risposta basata sulla richiesta.

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

## Setup

Per creare il nuovo `UmamiDataService` per usare questo handler è similemente semplice.

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

Voi' vedrete che ho appena messo in piedi il `ServiceCollection`, aggiungere il `FakeLogger<T>` ( vediamo ancora il [ultimo articolo per i dettagli su questo.](/blog/unittestinglogginginaspnetcore) E poi abbiamo messo in piedi il `UmamiData` il servizio con il nome di usuario e password che voglio usare ( così posso testare l'errore).

Poi chiamo. `services.SetupUmamiData(username, password);` che è un metodo di estensione che ho creato per creare il `UmamiDataService` con l'uso di `UmamiDataDelegatingHandler` e la `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>();
    }
```

Potete vedere che questo è il punto in cui mi imbatto. `UmamiDataDelegatingHandler` e la `AuthService` all'interno del sistema. `UmamiDataService`. Il modo in cui è strutturato è che il `AuthService` 'propriamente'. `HttpClient` e la `UmamiDataService` usa il `AuthService` per fare le chiamate all'API Umami con il `bearer` il tocco e `BaseAddress` già stabilito.

## I test

In realtà questo rende realmente il test molto semplice. E' è solo un po' verboso perché volevo anche testare l'affidamento tooM SK2 Tutto quello che fa ' è pubblicare attraverso il mio blog. `DelegatingHandler` e simulo una risposta basata sulla richiesta.

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

### Simulare la risposta

Per simulare la risposta per questo metodo vi ricorderò che ho questa linea nel `UmamiDataDelegatingHandler`:

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

Tutto ciò fa è tirare l'informazione dalla sequenza di query e costruire una risposta 'realisticaM SK1 basata su Test Live I'ho compilatoMSC4 ancora molto pochi docs su questo ). VoiMST6 vedrete che faccio un test per il numero di giorni tra la data di partenza e quella di fine e poi restituisco una risposta con lo stesso numero di anniMSSK7

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

# Conclusione

Quindi, è davvero molto semplice testare un test. `HttpClient` la richiesta senza usare Moq e penso che sia molto più pulita in questo modo.