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

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

Wednesday, 04 September 2024

//

8 minute read

Εισαγωγή

Στο προηγούμενο μέρος αυτής της σειράς που δοκιμάσαμε Ομάμι.Net μεθόδους παρακολούθησης

Το πρόβλημα

Στο προηγούμενο μέρος χρησιμοποιούσα το Moq για να μου δώσω ένα Mock<HttpMessageHandler> και επιστρέφουν τον εργοδότη που χρησιμοποιήθηκε UmamiClient, ίσως αυτό είναι ένα κοινό πρότυπο κατά την δοκιμή του κώδικα που χρησιμοποιεί HttpClientΣε αυτό το άρθρο θα σας δείξω πώς να δοκιμάσετε το νέο UmamiDataService Χωρίς να χρησιμοποιείτε Moq.

    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. Έτσι θέλω να παίξω με αυτά τα δεδομένα πήγα μέσα από την προσπάθεια δημιουργίας ενός πελάτη CΜSK1 για αυτό.

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

Μπορείτε να δείτε ότι το MUCH του κώδικα κατασκευάζει το QueryString από το παρελθόν στην κατηγορία PageViewsRequest ( υπάρχουν και άλλοι τρόποι για να το κάνετε αυτό, αλλά αυτό, για παράδειγμα χρησιμοποιώντας Attributes ή αντανακλαστικά λειτουργούν εδώM SK2

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

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

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

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

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 Η χρήση αυτού του χειριστή είναι εξίσου απλή.

    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> ( Ας δούμε το Τελευταίο άρθρο για λεπτομέρειες σχετικά με αυτό Και στη συνέχεια δημιουργήστε το UmamiData υπηρεσία με το όνομα χρήστη και τον κωδικό πρόσβασης που θέλω να χρησιμοποιήσω (so μπορώ να δοκιμάσω την αποτυχία).

Στη συνέχεια θα καλέσω services.SetupUmamiData(username, password); είναι μια μέθοδος επέκτασης που δημιούργησα για να UmamiDataService Με την UmamiDataDelegatingHandler Και η AuthService;

    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 και προσομοιώνω μια απάντηση με βάση το αίτημα.

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:

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

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

    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 διατηρείται μια καλή εμπορική αναμέτρηση στην . διατηρούν

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.