# Μονάδα δοκιμής Umami.Net - Δοκιμή δεδομένων Umami χωρίς τη χρήση Moq

# Εισαγωγή

Στο προηγούμενο μέρος αυτής της σειράς που δοκιμάσαμε[ Ομάμι.Net μεθόδους παρακολούθησης ](/blog/unittestingumaminet)

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

## Το πρόβλημα

Στο προηγούμενο μέρος χρησιμοποιούσα το Moq για να μου δώσω ένα `Mock<HttpMessageHandler>` και επιστρέφουν τον εργοδότη που χρησιμοποιήθηκε `UmamiClient`, ίσως αυτό είναι ένα κοινό πρότυπο κατά την δοκιμή του κώδικα που χρησιμοποιεί `HttpClient`Σε αυτό το άρθρο θα σας δείξω πώς να δοκιμάσετε το νέο `UmamiDataService` Χωρίς να χρησιμοποιείτε 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;
    }
```

## Γιατί να χρησιμοποιήσετε Moq?

Το Moq είναι μια ισχυρή βιβλιοθήκη μολύβωσης που σας επιτρέπει να δημιουργήσετε αντικείμενα μολυβισμού για διεπαφές και τάξεις. Χρησιμοποιείται ευρέως σε δοκιμές μονάδων για την απομόνωση του κώδικα υπό δοκιμή από τις εξαρτήσεις τουΜSK1 Ωστόσο,, Υπάρχουν ορισμένες περιπτώσεις όπου η χρήση του Moq μπορεί να είναι συγκρατημένη ή ακόμη και αδύνατη ΜSK3 Για παράδειγμα, , όταν δοκιμάζετε τον κωδικό που χρησιμοποιεί στατικές μεθόδους ή όταν ο κωδικός κάτω από δοκιμασία συνδέεται στενά με τις εξάρσεις του

Το παράδειγμα που έδωσα παραπάνω δίνει μεγάλη ευελιξία στην δοκιμή των `UmamiClient` Κλάση, αλλά έχει επίσης μερικά μειονεκτήματα. ΑυτόMK2 UGLY κώδικα και κάνει πολλά πράγματα που δενΜSK3 πραγματικά χρειάζονταιM SK4 Έτσι όταν δοκιμάζετε `UmamiDataService` Αποφάσισα να δοκιμάσω μια διαφορετική προσέγγιση.

# Δοκιμή Υπηρεσιών

Η `UmamiDataService` είναι μια μελλοντική προσθήκη στη βιβλιοθήκη του Umami.Net που θα σας επιτρέψει να συλλέξετε δεδομένα από το Umami για πράγματα όπως βλέποντας πόσες προβολές μια σελίδα είχε, τι γεγονότα συνέβη σε ένα συγκεκριμένο είδοςM SK2 φιλτραρίστηκε από έναν τόνο παραμέτρων ράβδος χώραMSKA3 πόληMKA4 OSMSKI5 μεγέθους οθόνηςMKI6 κλπMK7 Αυτό είναι πολύ ισχυρό αλλά τώρα η [Το Umami API λειτουργεί μόνο μέσω JavaScript](https://umami.is/docs/api/website-stats). Έτσι θέλω να παίξω με αυτά τα δεδομένα πήγα μέσα από την προσπάθεια δημιουργίας ενός πελάτη CΜSK1 για αυτό.

Η `UmamiDataService` Η τάξη χωρίζεται σε πολύπλευρες μερικές τάξεις (Οι μέθοδοι είναι SUPER μακρύςΜSK1 για παράδειγμα εδώ' `PageViews` Μεθοδολογία.

Μπορείτε να δείτε ότι το MUCH του κώδικα κατασκευάζει το QueryString από το παρελθόν στην κατηγορία PageViewsRequest ( υπάρχουν και άλλοι τρόποι για να το κάνετε αυτό, αλλά αυτό, για παράδειγμα χρησιμοποιώντας Attributes ή αντανακλαστικά λειτουργούν εδώM 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>
Όπως μπορείτε να δείτε αυτό πραγματικά απλά κατασκευάζει μια γραμμή αναζήτησης. σίγουρα επαληθεύει την κλήση ( σίγουρα [Τελευταίο άρθρο](/blog/unittestinglogginginaspnetcore) για ορισμένες λεπτομέρειες σχετικά με αυτό) και στη συνέχεια κάνει την κλήση στο Umami API. Έτσι πώς δοκιμάζουμε αυτό

## Δοκιμάστε το UmamiDataService

Σε αντίθεση με την δοκιμή UmamiClient, αποφάσισα να δοκιμάσω το `UmamiDataService` Χωρίς να χρησιμοποιείτε το Moq. Αντί για το , δημιούργησα ένα απλό `DelegatingHandler` τάξη που μου επιτρέπει να ρωτήσω το αίτημα και στη συνέχεια να επιστρέψω μια απάντηση. Αυτή είναι μια πολύ πιο απλή προσέγγιση από τη χρήση του Moq και μου επιτρέπουν να δοκιμάσω την `UmamiDataService` Χωρίς να χτυπάει η `HttpClient`.

Στο κώδικα παρακάτω μπορείτε να δείτε ότι απλώς επεκτείνω `DelegatingHandler` και να ξεπεράσει το `SendAsync` Μέθοδος.Αυτή η μέθοδος μου επιτρέπει να ελέγξω το αίτημα και να επιστρέψω μια απάντηση βάσει του αιτήματοςΜSK1

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

## Ρυθμίσεις

Για να δημιουργήσουμε το νέο `UmamiDataService` Η χρήση αυτού του χειριστή είναι εξίσου απλή.

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

Σας αρέσει το ' αναμένεται να δείτε ότι μόλις εγκατέστησα την `ServiceCollection`, προσθέστε το `FakeLogger<T>` ( Ας δούμε το [Τελευταίο άρθρο για λεπτομέρειες σχετικά με αυτό](/blog/unittestinglogginginaspnetcore) Και στη συνέχεια δημιουργήστε το `UmamiData` υπηρεσία με το όνομα χρήστη και τον κωδικό πρόσβασης που θέλω να χρησιμοποιήσω (so μπορώ να δοκιμάσω την αποτυχία).

Στη συνέχεια θα καλέσω `services.SetupUmamiData(username, password);` είναι μια μέθοδος επέκτασης που δημιούργησα για να `UmamiDataService` Με την `UmamiDataDelegatingHandler` Και η `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>();
    }
```

Μπορείς να δεις ότι αυτό είναι που βγαίνω μέσα στο `UmamiDataDelegatingHandler` Και η `AuthService` Για την `UmamiDataService`. Ο τρόπος με τον οποίο αυτό διαρθρώνεται είναι ότι η `AuthService` 'όλησεΜSK1ο `HttpClient` Και η `UmamiDataService` Χρησιμοποιήστε το `AuthService` για να κάνουμε τις κλήσεις στο API του Umami με την `bearer` Τόκεν και `BaseAddress` Έχουμε ξεκινήσει το .

## Οι δοκιμές

Στην πραγματικότητα αυτό κάνει πραγματικά να δοκιμάσετε αυτό πραγματικά απλό. Το' διατηρεί μόνο λίγο λεκτική καθώς ήθελα επίσης να ελέγξω τη σύνδεση πολύΜSK2 Όλα όσα τοM SK3 διατηρείται είναι δημοσιεύεται μέσω μου `DelegatingHandler` και προσομοιώνω μια απάντηση με βάση το αίτημα.

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

### Simulating την απάντηση

Για να προσομοιώσετε την απάντηση για αυτή τη μέθοδο θα σας αρέσει' ίσως θυμηθείτε ότι έχω αυτήν την γραμμή στο `UmamiDataDelegatingHandler`:

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

Όλα αυτά είναι να τραβήξετε πληροφορίες από το ερωτηματολόγιο και κατασκευάζει μια ' μη ρεαλιστική ΜSK1 απάντηση ΜSK2 βασισμένη σε ζωντανές δοκιμές ' συγκεντρώθηκε ΜΚ4 ξανά πολύ λίγα αποδεικτικά στοιχεία σχετικά με αυτό ΜΣΚ5 YouΜΣΚ6 θα δείτε εγώ δοκιμάζω για τον αριθμό των ημερών μεταξύ της αρχικής και τελικής ημερομηνίας και στη συνέχεια επιστρέψετε μια απάντηση με τον ίδιο αριθμό ημερών ΜСК7

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

# Στο συμπέρασμα

Ως εκ τούτου, το ' είναι πραγματικά ευχάριστο ότι η ' είναι αρκετά απλή για να δοκιμάσει ένα `HttpClient` Ζητήστε χωρίς να χρησιμοποιείτε Moq και νομίζω ότι το ' διατηρεί πολύ καθαρότερο με αυτόν τον τρόπο στο . διατηρείτε την απώλεια κάποιων από τις εξελίξεις που έγιναν δυνατές στο Moq, αλλά για απλές δοκιμές όπως αυτό στο , διατηρώ τη γνώμη μου ότι το ΜSK3 διατηρείται μια καλή εμπορική αναμέτρηση στην . διατηρούν