This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Unit Testing Umami.Net M SK1 Testing umami Data Without Using Moq
Wednesday, 04 September 2024
Введение
У попередньому частині цієї серії я тестував Umami.Методи стеження за мережею
Проблема
В попередній частині я використовував 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 - це потужна модікаційна бібліотека, яка дозволяє створювати макетні об 'єкти для інтерфейсів та класи. Її широко використовують в тестуванні на одиницях для ізоляції коду під тестуванням від його залежностей. ОднакM SK2 є певні випадки, коли використання Moq може бути важким або навіть неможливим . НаприкладMSC4 при тестуванні коду, який використовує статичні методи або коли код під тестацією тісно пов' язаний з його заleżністю.
У прикладі, що я показав раніше, є велика гнучкість під час тестування UmamiClient клас, але він також має деякі недоліки . ВінM SK2s UGLY код і робить багато речей, які я не маю' не дійсно потрібноMSC4 Так що коли тестує UmamiDataService Я вирішив спробувати інший підхід.
Випробовування UmamiDataService
І UmamiDataService є майбутнім доповненням до Umami.Net бібліотеки, що дозволить вам завантажити дані з Umami для таких речей, як бачити, скільки переглядів на сторінці було , які події відбувалися певного типу МSK2 відфільтровані тонною параметрів liek країна MSC3 місто M SK4 ОС , розмір екрану Mスク6 і т.д. API Umami працює лише за допомогою JavaScript. Отже, щоб гратися з цими даними, я спробував створити для нього C # клієнта
І UmamiDataService клас поділений на багатовимірні частинні класи (metodи SUPER довгі ) наприклад, тут МSK2с PageViews метод.
Ви бачите, що МАК коду конструює QueryString від passed в klasiі PageViewsRequest ( є й інші способи зробити це, але це , наприклад, використовуючи Attributes або Reflection jobs тут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);
}
Тестування 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> ( ще раз погляньте на останній artykuł для деталей про це а потім створити UmamiData послуга з ім 'ям користувача і паролем, яке я хочу використати ( щоб перевірити невдачі
Тоді я викликав 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 Умамі з 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 SK1 відповідь МSK2на основі живих тестів ЯMSC3с Comppiled, знову дуже малі Docs на цьому ). ВиMST6 побачите, я перевіряю число днів між датою початку та кінця, а потім повертаю відповідь з таким же числом днів
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 вимагати без використання Моку, і я думаю, що це "'" набагато чистіше таким чином . МSK1 Ви втрачаєте деякі складності, які були можливими для Moq , але для простих тестів, таких як цей.