# اختبار وحدة أوامري.شبكة -اختبار клиент أوامری

# المقدمة

الآن لدي [امامي.حزمة الإنترنت](https://www.nuget.org/packages/Umami.Net/) هناك بالطبع أريد أن أتأكد من أن كل شيء يعمل كما كان متوقعا.
ملاحظة: هذه ليست ' طريقة ' مثالية ' نوع المراسلة , إنها ' هي فقط الطريقة التي قمت بها أنا ' أنا فعلتها حالياً . في الواقع أنا لا أحتاج ' حقاً لتحريك `IHttpMessageHandler` هنا يمكنك الهجوم على مسند رسالة الإتصال إلى مستخدم Http عادي للقيام بهذا. أردت فقط أن أريكم كيف يمكنك القيام بذلك مع ماك

[TOC]

<!--category-- xUnit, Umami -->
<datetime class="hidden">2024-09-01T17:22</datetime>

# اختبار وحدة

التجريب بالوحدات يشير إلى عملية اختبار وحدة واحدة من البرمجة للتأكد من أنها تعمل كما هو متوقع.

بالنسبة لحزمة مثل Umami.Net هذا معقد نوعا ما لأنها تتصل بمستخدم بعيد `HttpClient` ولديه `IHostedService` يستخدم لجعل إرسال بيانات الأحداث الجديدة سلسة قدر الإمكان.

## اختبار UmamiClient

الجزء الأكبر من الاختبار `HttpClient` مكتبة مبنية على avoiding the actual 'HttpClientM SK1 call. This is done by creating a `HttpClient` التي تستخدم `HttpMessageHandler` الذي يعيد استجابة معروفة. هذا يتم عن طريق إنشاء `HttpClient` مع `HttpMessageHandler` الذي يعيد استجابة معروفة ; في هذه الحالة أقوم فقط بإعادة الاستجابة الواردة والتأكد من أن ' لم ينسق من قبل `UmamiClient`.

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

كما ترون، سيقوم هذا بإعداد `Mock<HttpMessageHandler>` ثم أنتقل إلى `UmamiClient`.
في هذا الرمز أربط هذا في `IServiceCollection` طريقة الإعداد. هذا يضيف كل الخدمات المطلوبة من قبل `UmamiClient` بما في ذلك `HttpMessageHandler` ومن ثم يعيد `IServiceCollection` للاستعمال في الإختبارات.

```csharp
    public static IServiceCollection SetupServiceCollection(string webSiteId = Consts.WebSiteId,
        string umamiPath = Consts.UmamiPath, HttpMessageHandler? handler = null)
    {
        var services = new ServiceCollection();
        var umamiClientSettings = new UmamiClientSettings
        {
            WebsiteId = webSiteId,
            UmamiPath = umamiPath
        };
        services.AddSingleton(umamiClientSettings);
        services.AddScoped<PayloadService>();
        services.AddLogging(x => x.AddConsole());
        // Mocking HttpMessageHandler with Moq
        var mockHandler = handler ?? EchoMockHandler.Create();
        services.AddHttpClient<UmamiClient>((serviceProvider, client) =>
        {
            var umamiSettings = serviceProvider.GetRequiredService<UmamiClientSettings>();
            client.BaseAddress = new Uri(umamiSettings.UmamiPath);
        }).ConfigurePrimaryHttpMessageHandler(() => mockHandler);
        return services;
    }
```

لإستخدام هذا وحقنه في `UmamiClient` ثم أستخدم هذه الخدمات في `UmamiClient` الإعداد.

```csharp
    public static UmamiClient GetUmamiClient(IServiceCollection? serviceCollection = null,
        HttpContextAccessor? contextAccessor = null)
    {
        serviceCollection ??= SetupServiceCollection();
        SetupUmamiClient(serviceCollection, contextAccessor);
        if (serviceCollection == null) throw new NullReferenceException(nameof(serviceCollection));
        var serviceProvider = serviceCollection.BuildServiceProvider();
        return serviceProvider.GetRequiredService<UmamiClient>();
    }
```

سوف ترون أن لدي هنا مجموعة من العوامل الإختيارية البديلة التي تسمح لي بإدخال خيارات مختلفة لمختلف أنواع الاختبار

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

إذن الآن لدي كل هذا الإعداد في مكاني يمكنني الآن أن أبدأ كتابة الإختبارات `UmamiClient` الطرق.

#### أرسل

ما يعنيه كل هذا الإعداد هو أن اختباراتنا في الواقع يمكن أن تكون بسيطة جداً

```csharp
public class UmamiClient_SendTests
{
    [Fact]
    public async Task Send_Wrong_Type()
    {
        var umamiClient = SetupExtensions.GetUmamiClient();
        await Assert.ThrowsAsync<ArgumentException>(async () => await umamiClient.Send(type: "boop"));
    }

    [Fact]
    public async Task Send_Empty_Success()
    {
        var umamiClient = SetupExtensions.GetUmamiClient();
        var response = await umamiClient.Send();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}
```

هنا ترون أبسط حالت الاختبار, فقط من أجل ضمان أن `UmamiClient` يمكننا إرسال رسالة والحصول على استجابة; بشكل مهم نحن أيضا نختبر حالة إستثناء حيث `type` هو خطأ. هذا جزء غالبا ما يتجاهله في الاختبار, للتأكد من أن الشفرة تفشل كما توقعت

#### رؤية الصفحة

لاختبار طريقتنا لرؤية الصفحات يمكننا القيام بشيء مماثل. في الشفرة التالية أستخدم `EchoHttpHandler` لتعكس ردة الفعل التي أرسلتها وتتأكد من أن تبعث ما أتوقعه

```csharp
    [Fact]
    public async Task TrackPageView_WithNoUrl()
    {
        var defaultUrl = "/testpath";
        var contextAccessor = SetupExtensions.SetupHttpContextAccessor(path: "/testpath");
        var umamiClient = SetupExtensions.GetUmamiClient(contextAccessor: contextAccessor);
        var response = await umamiClient.TrackPageView();

        var content = await response.Content.ReadFromJsonAsync<EchoedRequest>();
        Assert.NotNull(response);
        Assert.NotNull(content);
        Assert.Equal(content.Payload.Url, defaultUrl);
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
```

### مُصل للمحتوى HttpContext

هذا يستخدم `HttpContextAccessor` لتحديد الطريق إلى `/testpath` ومن ثم يتأكد من أن `UmamiClient` يرسل هذا بشكل صحيح.

```csharp
    public static HttpContextAccessor SetupHttpContextAccessor(string host = Consts.Host,
        string path = Consts.Path, string ip = Consts.Ip, string userAgent = Consts.UserAgent,
        string referer = Consts.Referer)
    {
        HttpContext httpContext = new DefaultHttpContext();
        httpContext.Request.Host = new HostString(host);
        httpContext.Request.Path = new PathString(path);
        httpContext.Connection.RemoteIpAddress = IPAddress.Parse(ip);
        httpContext.Request.Headers.UserAgent = userAgent;
        httpContext.Request.Headers.Referer = referer;

        var context = new HttpContextAccessor { HttpContext = httpContext };
        return context;
    }

```

هذا مهم بالنسبة لبرمجة زميلي Umami لأن الكثير من البيانات التي ترسل من كل طلب في الواقع يتم توليدها ديناميكيا من `HttpContext` object. إذن لا يمكننا إرسال أي شيء على الإطلاق في `await umamiClient.TrackPageView();` وترسل البيانات الصحيحة عن طريق استخراج ال Url من `HttpContext`.

كما سنرى لاحقاً هو أيضاً مهم أن ترسل الأشياء مثل `UserAgent` و `IPAddress` كما أن هذه تستخدم من قبل server Umami لمتابعة البيانات و 'track' مشاهد المستخدم بدون استخدام cookiesM SK2

من أجل الحصول على هذا التنبؤ يمكننا تعريف مجموعة من المحافظات في `Consts` class. لذا يمكننا اختبارها ضد الاستجابات المتوقعة والطلبات

```csharp
public class Consts
{
    public const string UmamiPath = "https://example.com";
    public const string WebSiteId = "B41A9964-FD33-4108-B6EC-9A6B68150763";
    public const string Host = "example.com";
    public const string Path = "/example";
    public const string Ip = "127.0.0.1";
    public const string UserAgent = "Test User Agent";
    public const string Referer = "Test Referer";
    public const string DefaultUrl = "/testpath";
    public const string DefaultTitle = "Example Page";
    public const string DefaultName = "RSS";
    public const string DefaultType = "event";

    public const string Email = "test@test.com";

    public const string UserId = "11224456";
    
    public const string UserName = "Test User";
    
    public const string SessionId = "B41A9964-FD33-4108-B6EC-9A6B68150763";
}
```

## اختبارات أخرى

هذا فقط بداية استراتيجية اختبارنا لـ Umami.Net, لا يزال علينا اختبار `IHostedService` واختبارها ضد البيانات الحقيقية ينتج "umami" ( والذي لا يتم وثيقه في أي مكان بل يحتوي على رمز JWT مع بعض البيانات المفيدة

```json
{
  "alg": "HS256",
  "typ": "JWT"
}{
  "id": "b9836672-feee-55c5-985a-a5a23d4a23ad",
  "websiteId": "32c2aa31-b1ac-44c0-b8f3-ff1f50403bee",
  "hostname": "example.com",
  "browser": "chrome",
  "os": "Windows 10",
  "device": "desktop",
  "screen": "1920x1080",
  "language": "en-US",
  "country": "GB",
  "subdivision1": null,
  "subdivision2": null,
  "city": null,
  "createdAt": "2024-09-01T09:26:14.418Z",
  "visitId": "e7a6542f-671a-5573-ab32-45244474da47",
  "iat": 1725182817
}2|Y*: �(N%-ޘ^1>@V
```

لذا سنريد ' أن نختبر ذلك , نحاكي الرموز وربما نعيد البيانات في كل زيارة `uuid(websiteId,ipaddress, useragent)`).

# في الختام

هذا فقط بداية اختبار التجزئة الشبكية Umami., هنالك' هناك الكثير لفعله لكن هذا هو البداية الجيدة . سأقوم بإضافة المزيد من الإختبارات كلما ذهبت ولا شك في تحسين هذه