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

# المقدمة

في الجزء السابق من هذه السلسلة حيث قمت باختبار[ امامي.أساليب تتبع الشبكة ](/blog/unittestingumaminet)

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

## المشكلة

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

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

# اختبار UmamiDataService

الـ `UmamiDataService` هو إضافة مستقبلية إلى مكتبة Umami.Net التي ستسمح لك بأخذ البيانات من Umami لأشياء مثل رؤية كم عدد المشاهد التي كانت لها صفحة, ما حدث من نوع معينM SK2 تم فرزها بطن من العوامل liek الدولةMSC3 المدينة , نظام OSMska5 حجم الشاشةMske6 إلخMسک7 هذا قوي جداً لكن حالياً [واجهة "umami" تعمل فقط عبر JavaScript](https://umami.is/docs/api/website-stats). لذلك أردت أن ألعب مع تلك البيانات ذهبت من خلال جهودي لخلق نموذج مبني على C

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

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

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

## اختبار خدمة UmamiDataService

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

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

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

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

## الإختبارات

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

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

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

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

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