# Einheitenprüfung Umami-.-Net Testen von Umamidaten ohne Verwendung von Moq

# Einleitung

Im vorherigen Teil dieser Serie, wo ich getestet[ Umami-.-Net-Tracking Methoden ](/blog/unittestingumaminet)

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

## Das Problem

Im vorherigen Teil habe ich Moq benutzt, um mir eine `Mock<HttpMessageHandler>` und den benutzten Handler zurückgeben `UmamiClient`Dies ist ein häufiges Muster beim Testen von Code, das `HttpClient`In diesem Beitrag werde ich Ihnen zeigen, wie man die neue testen `UmamiDataService` ohne Verwendung von 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;
    }
```

## Warum verwenden Sie Moq-??

Moq ist eine leistungsstarke Spoting-Bibliothek, mit der Sie Mock-Objekte für Schnittstellen und Klassen erstellen können.Es wird weit verbreitet in Unit-Tests verwendet, um den Code unter Test von seinen Abhängigkeiten zu isolieren..... Jedoch gibt es einige Fälle, in denen die Verwendung von Moq schwerfällig oder sogar unmöglich sein kann.

Das Beispiel, das ich oben gab, gibt eine Menge Flexibilität beim Testen der `UmamiClient` Klasse - ,, aber es hat auch einige Nachteile , . , Es ist UGLY-Code und tut eine Menge von Sachen, die ich nicht wirklich brauchen , . Also beim Testen `UmamiDataService` Ich entschied mich, einen anderen Ansatz zu versuchen.

# Testen von UmamiDataService

Das `UmamiDataService` ist eine zukünftige Ergänzung zu der Umami-.-Net-Bibliothek, die Ihnen erlauben wird, Daten von Umami für Sachen wie sehen, wie viele Ansichten eine Seite hatte ., , welche Ereignisse eines bestimmten Typs geschahen ?, , gefiltert durch eine Tonne Parameter Liek country . [Umami API funktioniert nur über JavaScript](https://umami.is/docs/api/website-stats)So wollend, mit diesen Daten zu spielen Ich ging durch die Mühe der Schaffung eines Clients für es C.#...

Das `UmamiDataService` class ist bis in multple Teilklassen unterteilt. Die Methoden sind SUPER lang. `PageViews` Methode:

Sie können sehen, dass MUCH des Codes die QueryString aus der übergebenen in PageViewsRequest class baut.Es gibt andere Möglichkeiten, dies zu tun, aber dies...,... zum Beispiel mit Attributen oder Reflexion arbeitet hier...

<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>
Wie Sie sehen können, baut dies wirklich nur eine Abfrage string-.. , authentifiziert den Aufruf [letzter Artikel](/blog/unittestinglogginginaspnetcore) für einige Details auf diesem und macht dann den Anruf an die Umami API, also wie testen wir diese

## Testen des UmamiDataService

Im Gegensatz zum Testen von UmamiClient-, testete ich die `UmamiDataService` ohne die Verwendung von Moq-., stattdessen, habe ich eine einfache `DelegatingHandler` Klasse, die es mir erlaubt, die Anfrage zu verhören und dann eine Antwort zurückgeben Dies ist ein viel einfacherer Ansatz als mit Moq und ermöglicht es mir zu testen, die `UmamiDataService` ohne zu verspotten die `HttpClient`.

Im Code unten sehen Sie, dass ich einfach expandiere `DelegatingHandler` und überschreiben `SendAsync` Methode: Diese Methode erlaubt es mir, die Anfrage zu prüfen und eine Antwort basierend auf der Anfrage zurückzugeben.

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

## Einrichtung

Um die neue `UmamiDataService` um diesen Handler zu verwenden ist ähnlich einfach

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

Sie sehen, dass ich gerade die `ServiceCollection`Einfügen der `FakeLogger<T>` Siehe auch: [letzter Artikel für Details zu diesem](/blog/unittestinglogginginaspnetcore) und dann die `UmamiData` service mit dem Benutzernamen und Passwort Ich möchte verwenden, um (.so kann ich Testfehler.)..

Ich rufe dann in `services.SetupUmamiData(username, password);` das ist eine Erweiterungsmethode, die ich erstellt habe, um die `UmamiDataService` mit der `UmamiDataDelegatingHandler` und der `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>();
    }
```

Sie können sehen, dass dies ist, wo ich Haken in der `UmamiDataDelegatingHandler` und der `AuthService` zu dem `UmamiDataService`Die Art und Weise, wie dies ist strukturiert ist, dass die `AuthService` ',owns ,', `HttpClient` und der `UmamiDataService` verwendet die `AuthService` um die Anrufe zur Umami API mit dem `bearer` Zeichen und `BaseAddress` bereits eingestellt

## Die Prüfungen

Wirklich macht dies tatsächlich das Testen dieses wirklich einfach ,. , es ist nur ein bisschen verbal, da ich auch die Protokollierung zu testen wollte , . , Alles was es tun, ist durch meine Posting `DelegatingHandler` und ich simulieren eine Antwort basierend auf der Anfrage

```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 der Reaktion

Um die Antwort für diese Methode zu simulieren Sie sich an mich erinnern, habe ich diese Zeile in der `UmamiDataDelegatingHandler`:

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

Alles, was dies tut, ist Informationen aus dem Querystring zu ziehen und konstruiert eine sehr kleine docs auf diesem I)..You.'.ll sehen Ich teste für die Anzahl der Tage zwischen Start- und Enddatum und dann geben Sie eine Antwort mit der gleichen Anzahl von Tagen.

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

# Schlussfolgerung

So dass es wirklich es ist, ' ziemlich einfach zu testen ein `HttpClient` Anfrage ohne Verwendung von Moq und ich denke, es ist weit sauberer auf diese Weise... Sie verlieren einige der Raffinesse in Moq möglich gemacht aber für einfache Tests wie diese...