اختبار وحدة الأمامي.Net - اختبار بيانات الأمامي دون استخدام موك (العربية (Arabic))

اختبار وحدة الأمامي.Net - اختبار بيانات الأمامي دون استخدام موك

Wednesday, 04 September 2024

//

7 minute read

المقدمة

في الجزء السابق من هذه السلسلة حيث قمت باختبار امامي.أساليب تتبع الشبكة

المشكلة

في الجزء السابق استخدمت "موك" لأعطيني 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 عبارة عن مكتبة مزج قوية تمكنك من إنشاء أجسام مزجة لواجهات وقواعد.. تستخدم بشكل واسع في الاختبارات الفردية لتمييز الشفرة المُختبرة من إعتماداتها.

المثال الذي أعطيته سابقا يعطي الكثير من المرونة في اختبار UmamiClient الكلية, لكنها أيضاً لديها بعض drawbacks. إنها برمجة UGLY و تقوم بكثير من الأشياء التي لا أحتاج إليها UmamiDataService قررت أن أحاول مقاربة مختلفة

اختبار UmamiDataService

الـ UmamiDataService هو إضافة مستقبلية إلى مكتبة Umami.Net التي ستسمح لك بأخذ البيانات من Umami لأشياء مثل رؤية كم عدد المشاهد التي كانت لها صفحة, ما حدث من نوع معينM SK2 تم فرزها بطن من العوامل liek الدولةMSC3 المدينة , نظام OSMska5 حجم الشاشةMske6 إلخMسک7 هذا قوي جداً لكن حالياً واجهة "umami" تعمل فقط عبر JavaScript. لذلك أردت أن ألعب مع تلك البيانات ذهبت من خلال جهودي لخلق نموذج مبني على C

الـ UmamiDataService يقسم الصنف إلى مجموعات جزئية متعددة (الأساليب طويلة جداً ) على سبيل المثال هنا PageViews الطريقة.

يمكنكم أن تروا أن الكثير من البرمجة تقوم ببناء QueryString من ما تم إعطائه في فئة PageViewsRequest (هناك طرق أخرى لفعل ذلك لكن هذا, على سبيل المثال باستخدام السمات أو أعمال الإنعكاس هنا

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) لمعرفة بعض التفاصيل حول هذا ) ثم يتصل بAPI أمامي . إذن كيف نختبر هذا

اختبار خدمة UmamiDataService

على النقيض من اختبار UmamiClient, قررت اختبار UmamiDataService دون استخدام Moq. بدلاً من , قمت بإنشاء DelegatingHandler الكلية التي تسمح لي بإجراء استجواب الطلب ثم إعادة الإجابة. هذا مقاربة أبسط بكثير من استخدام Moq وتسمح لي باختبار UmamiDataService دون الحاجة للسخرية HttpClient.

في الشفرة التالية يمكنكم أن تروا أنا ببساطة أتوسع DelegatingHandler وتخطى SendAsync الطريقة. هذه الطريقة تسمح لي بفحص الطلب وتعيد استجابة بناء على الطلب

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 خدمة مع username وパスワード أريد أن أستخدم (لذلك أستطيع إختبار الفشل).

ثم أتصل 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 'owns' the HttpClient و UmamiDataService يستخدم AuthService لإجراء المكالمات إلى واجهة "أمامي" مع bearer الرموز و BaseAddress تم ضبطها بالفعل.

الإختبارات

حقاً هذا يجعل في الواقع اختبار هذا ببساطة جداً 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);
    }
}

محاكاة الاستجابة

ولحاكاة الاستجابة لهذا الأسلوب سوف تتذكرون ' لدي هذا الخط في UmamiDataDelegatingHandler:

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

كل ما يفعله هذا هو سحب المعلومات من سطر الاستفسار وبناء 'واقعي' استجابة (تبني على التجارب الحيّة لقد قمت بجمعM SK4 مرة أخرى بقليل جدا من الملاحظات على هذاMSC5 ستشاهدون أنني أقوم باختبار عدد الأيام بين يوم البداية وآخر ثم أعيد استجابته بنفس عدد الأيامMسک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 طلب دون استخدام موك وأعتقد أنّه

Finding related posts...
logo

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