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à di test Umami.Net - Test dei dati Umami senza usare Moq
Wednesday, 04 September 2024
Introduzione
Nella parte precedente di questa serie dove ho testato Umami.Methodi di tracciamento del network
Il problema
Nella parte precedente ho usato Moq per darmi un'idea. Mock<HttpMessageHandler> e restituire il handler usato in UmamiClient, questo è un modello comune quando si testa il codice che usa HttpClient. In questo post vi mostrerò come testare il nuovo. UmamiDataService senza usare 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;
}
Perché usare Moq?
Moq è una potente biblioteca di mocking che ti permette di creare oggetti da modellare per interfacce e classi. È largamente usata nell'unità di test per isolare il codice sotto controllo dalle sue dipendenze. TuttaviaM SK2 ci sono alcuni casi in cui l'uso di Moq può essere pesante o persino impossibile . Per esempioMSC4 quando viene testato un codice che usa metodi statici o quando il codice subposto è stretto agganciato alle sue dipendenza
L'esempio che vi ho dato ci dà molta flessibilità per testare. UmamiClient class, ma ha anche alcuni difetti. Ha il UGLY code di ' e fa un sacco di cose che non ne ho bisogno. UmamiDataService Ho deciso di provare un approccio diverso.
Testare UmamiDataService
L'informazione figura nella parte dispositiva. UmamiDataService è un'addizione futura alla biblioteca Umami.Net che vi permetterà di raccogliere dati da Umami per cose come vedere quante visuali aveva una pagina, quali eventi si sono verificati di un certo tipoM SK2 filtrati da una tonnellata di parametri liek country , cittàMST4 OSM ST5 dimensione dello schermoMst6 eccM st7 Questo è molto potente ma proprio ora la Umami API funziona solo attraverso JavaScript.. Quindi volevo giocare con quei dati e ho cercato di creare un cliente per lui.
L'informazione figura nella parte dispositiva. UmamiDataService La classe è divisa in classi partiali multiple (le metodi sono SUPER lungheM SK1 per esempio qui's la PageViews metodo.
Potete vedere che MUCH del codice sta costruendo la QueryString dal passato nella classe PageViewsRequest (ci sono altri modi per farlo ma questo, per esempio usando Attribute o il lavoro di riflesso quiM 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);
}
Testare il UmamiDataService
Contrariamente al test UmamiClient, ho deciso di testare UmamiDataService senza usare Moq. Invece, Ho creato un semplice DelegatingHandler Classe che mi permette di interrogare la richiesta e poi restituire una risposta. Questo è un approccio molto più semplice rispetto all'uso del Moq e mi consente di testare il UmamiDataService senza dover ridicolo. HttpClient.
Nel codice sotto potete vedere che semplicemente estendo DelegatingHandler e superare il SendAsync metodo. Questo metodo mi permette di esaminare la richiesta e restituire una risposta basata sulla richiesta.
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);
}
}
}
Setup
Per creare il nuovo UmamiDataService per usare questo handler è similemente semplice.
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();
}
Voi' vedrete che ho appena messo in piedi il ServiceCollection, aggiungere il FakeLogger<T> ( vediamo ancora il ultimo articolo per i dettagli su questo. E poi abbiamo messo in piedi il UmamiData il servizio con il nome di usuario e password che voglio usare ( così posso testare l'errore).
Poi chiamo. services.SetupUmamiData(username, password); che è un metodo di estensione che ho creato per creare il UmamiDataService con l'uso di UmamiDataDelegatingHandler e la 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>();
}
Potete vedere che questo è il punto in cui mi imbatto. UmamiDataDelegatingHandler e la AuthService all'interno del sistema. UmamiDataService. Il modo in cui è strutturato è che il AuthService 'propriamente'. HttpClient e la UmamiDataService usa il AuthService per fare le chiamate all'API Umami con il bearer il tocco e BaseAddress già stabilito.
I test
In realtà questo rende realmente il test molto semplice. E' è solo un po' verboso perché volevo anche testare l'affidamento tooM SK2 Tutto quello che fa ' è pubblicare attraverso il mio blog. DelegatingHandler e simulo una risposta basata sulla richiesta.
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);
}
}
Simulare la risposta
Per simulare la risposta per questo metodo vi ricorderò che ho questa linea nel UmamiDataDelegatingHandler:
if (absPath.StartsWith($"/api/websites/{Consts.WebSiteId}/pageviews"))
{
var pageViews = GetParams<PageViewsRequest> (request);
return ReturnPageViewsMessage(pageViews);
}
Tutto ciò fa è tirare l'informazione dalla sequenza di query e costruire una risposta 'realisticaM SK1 basata su Test Live I'ho compilatoMSC4 ancora molto pochi docs su questo ). VoiMST6 vedrete che faccio un test per il numero di giorni tra la data di partenza e quella di fine e poi restituisco una risposta con lo stesso numero di anniMSSK7
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")
};
}
Conclusione
Quindi, è davvero molto semplice testare un test. HttpClient la richiesta senza usare Moq e penso che sia molto più pulita in questo modo.