# Eenheidstest Umami.Net -Testen van Umami Data zonder Moq te gebruiken

# Inleiding

In het vorige deel van deze reeks heb ik getest.[ Umami.Net-trackingmethoden ](/blog/unittestingumaminet)

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

## Het probleem

In het vorige deel gebruikte ik Moq om me een `Mock<HttpMessageHandler>` en terugvoer de handvater die gebruikt is in `UmamiClient`, dit is een gemeenschappelijk patroon bij het testen van code dat gebruik maakt van `HttpClient`. In deze post zal ik je laten zien hoe je de nieuwe test moet testen. `UmamiDataService` zonder Moq. te gebruiken

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

## Waarom Moq? gebruiken

Moq is een krachtige mocking-bibliotheek waarmee je objecten kunt maken voor interfaces en klasse.. Het wordt algemeen gebruikt in eenheidstests om de code te isoleren van haar afhangingen.

Het voorbeeld dat ik eerder gaf, geeft veel flexibiliteit in het testen van de `UmamiClient` class, maar het heeft ook een paar drawbacks. Het is UGLY code en doet een heleboel dingen die ik niet nodig heb. `UmamiDataService` Ik besloot een andere aanpak te proberen.

# Testen van UmamiDataService

De gemeenschappelijke wetgeving van het land. `UmamiDataService` is een toekomstige toevoeging aan de Umami.Net-bibliotheek waarmee je gegevens van Umami kan halen om dingen te zien zoals hoeveel aansigten er op een pagina waren. [Umami API werkt alleen via JavaScript.](https://umami.is/docs/api/website-stats). Ik wilde dus met die gegevens spelen en ging er een C-client voor maken.

De gemeenschappelijke wetgeving van het land. `UmamiDataService` class is verdeeld in veelvuldige gedeeltelijke klasse (de methodes zijn SUPER lang) bijvoorbeeld hierM SK2s de `PageViews` methode.

Je kunt zien dat MUCH van de code de QueryString bouwt uit het doorgegeven in PageViewsRequest klas (er zijn andere manieren om dit te doen maar deze, bijvoorbeeld met Attributen of reflectiewerk hierM 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>
Zoals je kunt zien bouwt dit gewoon een zoek String . de roep authentifieert. [laatste artikel](/blog/unittestinglogginginaspnetcore) voor een paar details over dit, en dan roept hij naar de Umami API.

## De UmamiDataService testen

In tegenstelling tot het testen van UmamiClient, besloot ik de `UmamiDataService` zonder Moq. Ik creëerde een eenvoudige `DelegatingHandler` Dit is een veel eenvoudiger aanpak dan Moq gebruiken en ik kan de vraag testen. `UmamiDataService` Ik hoefde het niet te bespoten. `HttpClient`.

In de code onder kan je zien dat ik eenvoudig uitbreng. `DelegatingHandler` en overskryf de `SendAsync` methode. Deze methode laat me toe om de vraag te controleren en een antwoord terug te geven gebaseerd op de vraag.

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

## Opstelling

Om de nieuwe op te zetten. `UmamiDataService` om deze handvater te gebruiken is vergelijkbaar eenvoudig.

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

Je zal zien dat ik net een computer heb opgezet. `ServiceCollection`, voeg de `FakeLogger<T>` ( Kijk opnieuw naar de [laatste artikel voor details hierover.](/blog/unittestinglogginginaspnetcore) En dan zetten we de computers op. `UmamiData` service met de gebruikernaam en wachtwoord die ik wil gebruiken.

Dan bel ik in. `services.SetupUmamiData(username, password);` Dit is een uitbreidingsmethode die ik creëerde om de `UmamiDataService` met de `UmamiDataDelegatingHandler` en de `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>();
    }
```

Je kunt zien dat dit is waar ik in de `UmamiDataDelegatingHandler` en de `AuthService` naar de `UmamiDataService`. De manier waarop dit is gestructureerd, is dat de `AuthService` 'eigent' de `HttpClient` en de `UmamiDataService` het gebruik van de `AuthService` om aan de Umami API te bellen. `bearer` Token en `BaseAddress` al gezet.

## De tests

Dit maakt het testen echt heel eenvoudig. `DelegatingHandler` Ik simuleer een reactie gebaseerd op de vraag..

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

### Simulatie van de reactie

Om de reactie voor deze methode te simuleren, herinner je je aan dat ik deze lijn in de `UmamiDataDelegatingHandler`:

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

Dit haalt alleen informatie uit de zoekstring en bouwt een 'realistische' antwoord op basis van Live Tests. Ik heb 'omge compileerdM SK4 nogmaals heel weinig docs op dit.MSC5 Jullie zullen zien dat ik test voor het aantal dagen tussen de start- en einddatum en dan een antwoord terugvoer met hetzelfde aantal dagen.

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

# In slotting:

Het is dus heel eenvoudig om een test te doen. `HttpClient` request without using Moq and I think it's far cleaner this way. You DO lose some of the sophistication made possible in Moq but for simple tests like this