NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Tuesday, 03 September 2024
//Less than a minute
In het vorige artikel bespraken we hoe we de UmamiClient
gebruik van xUnit en Moq. In dit artikel bespreken we hoe we de UmamiBackgroundSender
Klas. De UmamiBackgroundSender
is een beetje anders dan UmamiClient
zoals het gebruikt IHostedService
om op de achtergrond te blijven draaien en verzoeken door te sturen UmamiClient
volledig uit de belangrijkste executive thread (dus het blokkeert de uitvoering niet).
Zoals gewoonlijk kunt u alle broncode voor dit zien op mijn GitHub Hier..
UmamiBackgroundSender
De werkelijke structuur van UmamiBackgroundSender
Het is heel eenvoudig. Het is een gehoste dienst die verzoeken stuurt naar de Umami server zodra het een nieuw verzoek detecteert. De basisstructuur UmamiBackgroundSender
De klasse is hieronder weergegeven:
public class UmamiBackgroundSender(IServiceScopeFactory scopeFactory, ILogger<UmamiBackgroundSender> logger) : IHostedService
{
private Channel<SendBackgroundPayload> _channel = Channel.CreateUnbounded<SendBackgroundPayload>();
private Task _sendTask = Task.CompletedTask;
public Task StartAsync(CancellationToken cancellationToken)
{
_sendTask = SendRequest(_cancellationTokenSource.Token);
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("UmamiBackgroundSender is stopping.");
// Signal cancellation and complete the channel
await _cancellationTokenSource.CancelAsync();
_channel.Writer.Complete();
try
{
// Wait for the background task to complete processing any remaining items
await Task.WhenAny(_sendTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
catch (OperationCanceledException)
{
logger.LogWarning("StopAsync operation was canceled.");
}
}
private async Task SendRequest(CancellationToken token)
{
logger.LogInformation("Umami background delivery started");
while (await _channel.Reader.WaitToReadAsync(token))
{
while (_channel.Reader.TryRead(out var payload))
{
try
{
using var scope = scopeFactory.CreateScope();
var client = scope.ServiceProvider.GetRequiredService<UmamiClient>();
// Send the event via the client
await client.Send(payload.Payload, type:payload.EventType);
logger.LogInformation("Umami background event sent: {EventType}", payload.EventType);
}
catch (OperationCanceledException)
{
logger.LogWarning("Umami background delivery canceled.");
return; // Exit the loop on cancellation
}
catch (Exception ex)
{
logger.LogError(ex, "Error sending Umami background event.");
}
}
}
}
private record SendBackgroundPayload(string EventType, UmamiPayload Payload);
}
Zoals je kunt zien is dit gewoon een klassieker. IHostedService
het is toegevoegd aan onze service collectie in ASP.NET met behulp van de services.AddHostedService<UmamiBackgroundSender>()
methode. Dit schopt de StartAsync
methode wanneer de toepassing begint.
De blik in de SendRequest
methode is waar de magie gebeurt. Hier lezen we van het kanaal en sturen we het verzoek naar de Umami server.
Dit sluit de werkelijke methoden voor het versturen van de verzoeken (hieronder weergegeven) uit.
public async Task TrackPageView(string url, string title, UmamiPayload? payload =null, UmamiEventData? eventData = null)
public async Task Identify(string? email = null, string? username = null,
string? sessionId = null, string? userId = null, UmamiEventData? eventData = null)
public async Task IdentifySession(string sessionId, UmamiEventData? eventData = null)
public async Task Track(string eventName, UmamiEventData? eventData = null)
public async Task Send(UmamiPayload? payload = null, UmamiEventData? eventData = null,
string eventType = "event")
De Voorzitter. - Het woord is aan de Fractie van de Europese Volkspartij (Christen-democratische Fractie). SendBackgroundPayload
Neem op en stuur het naar het kanaal.
Onze geneste ontvangen loop in SendRequest
zal blijven lezen vanaf het kanaal totdat het gesloten is. Hier zullen we onze testinspanningen concentreren.
while (await _channel.Reader.WaitToReadAsync(token))
{
while (_channel.Reader.TryRead(out var payload))
{
}
}
De achtergronddienst heeft een aantal semantiek die het mogelijk maken om gewoon af te schieten van het bericht zodra het aankomt.
Maar dit roept een probleem op; als we niet krijgen een geretourneerde waarde van de Send
Hoe testen we of dit echt iets doet?
UmamiBackgroundSender
Dus dan is de vraag hoe testen we deze dienst vijfen is er geen reactie om daadwerkelijk te testen tegen?
Het antwoord is het injecteren van een HttpMessageHandler
op de bespotte HttpClient die we naar onze UmamiClient sturen. Dit zal ons in staat stellen om het verzoek te onderscheppen en de inhoud ervan te controleren.
Je zult je herinneren uit het vorige artikel dat we een nep HttpMessageHandler hebben opgezet. Dit leeft in de EchoMockHandler
statische klasse:
public static class EchoMockHandler
{
public static HttpMessageHandler Create(
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responseFunc)
{
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync((HttpRequestMessage request, CancellationToken cancellationToken) =>
responseFunc(request, cancellationToken).Result);
return mockHandler.Object;
}
Je kunt hier zien dat we Mock gebruiken om een SendAsync
methode die een antwoord geeft op basis van het verzoek (in HttpClient worden alle async verzoeken gedaan via SendAsync
).
We zetten de Mock voor het eerst op.
var mockHandler = new Mock<HttpMessageHandler>();
Wij gebruiken dan de magie van Protected
voor het opzetten van de SendAsync
methode. Dit komt omdat... SendAsync
is normaal gesproken niet toegankelijk in de openbare API van HttpMessageHandler
.
public abstract class HttpMessageHandler : IDisposable
{
protected HttpMessageHandler()
{
}
protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
We gebruiken dan gewoon de catch-all ItExpr.IsAny
om aan elk verzoek te voldoen en het antwoord van de responseFunc
We passeren.
Binnenin de UmamiBackgroundSender_Tests
klasse hebben we een gemeenschappelijke manier om alle testmethoden te definiëren.
[Fact]
public async Task Track_Page_View()
{
var page = "https://background.com";
var title = "Background Example Page";
var tcs = new TaskCompletionSource<bool>();
// Arrange
var handler = EchoMockHandler.Create(async (message, token) =>
{
try
{
var responseContent = EchoMockHandler.ResponseHandler(message, token);
var jsonContent = await responseContent.Result.Content.ReadFromJsonAsync<EchoedRequest>(token);
var content = new StringContent("{}", Encoding.UTF8, "application/json");
Assert.Contains("api/send", message.RequestUri.ToString());
Assert.NotNull(jsonContent);
Assert.Equal(page, jsonContent.Payload.Url);
Assert.Equal(title, jsonContent.Payload.Title);
// Signal completion
tcs.SetResult(true);
return new HttpResponseMessage(HttpStatusCode.OK) { Content = content };
}
catch (Exception e)
{
tcs.SetException(e);
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
});
var (backgroundSender, hostedService) = GetServices(handler);
var cancellationToken = new CancellationToken();
await hostedService.StartAsync(cancellationToken);
await backgroundSender.TrackPageView(page, title);
var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(1000, cancellationToken));
if (completedTask != tcs.Task)
{
throw new TimeoutException("The background task did not complete in time.");
}
await tcs.Task;
await backgroundSender.StopAsync(CancellationToken.None);
}
Zodra we dit gedefinieerd hebben moeten we onze IHostedService
de levensduur van de testmethode:
var (backgroundSender, hostedService) = GetServices(handler);
var cancellationToken = new CancellationToken();
await hostedService.StartAsync(cancellationToken);
await backgroundSender.TrackPageView(page, title);
var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(1000, cancellationToken));
if (completedTask != tcs.Task)
{
throw new TimeoutException("The background task did not complete in time.");
}
await tcs.Task;
await backgroundSender.StopAsync(CancellationToken.None);
}
U kunt zien dat we in de handler naar onze GetServices
setup methode:
private (UmamiBackgroundSender, IHostedService) GetServices(HttpMessageHandler handler)
{
var services = SetupExtensions.SetupServiceCollection(handler: handler);
services.AddScoped<UmamiBackgroundSender>();
services.AddScoped<IHostedService, UmamiBackgroundSender>(provider =>
provider.GetRequiredService<UmamiBackgroundSender>());
SetupExtensions.SetupUmamiClient(services);
var serviceProvider = services.BuildServiceProvider();
var backgroundSender = serviceProvider.GetRequiredService<UmamiBackgroundSender>();
var hostedService = serviceProvider.GetRequiredService<IHostedService>();
return (backgroundSender, hostedService);
}
Hier geven we in onze handler aan onze diensten om het te haken in de UmamiClient
Installeren.
Vervolgens voegen we het volgende toe: UmamiBackgroundSender
naar de service collectie en krijg de IHostedService
van de dienstverlener. Breng dit dan terug naar de testklas om het gebruik ervan mogelijk te maken.
Nu we al deze dingen klaar hebben, kunnen we simpelweg... StartAsync
de Hosted Service, gebruik het dan wachten tot het stopt:
await hostedService.StartAsync(cancellationToken);
await backgroundSender.TrackPageView(page, title);
var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(1000, cancellationToken));
if (completedTask != tcs.Task)
{
throw new TimeoutException("The background task did not complete in time.");
}
await tcs.Task;
await backgroundSender.StopAsync(CancellationToken.None);
Dit start de gehoste service, stuur het verzoek, wacht op het antwoord en stop de service.
De Voorzitter. - Het woord is aan de Fractie van de Europese Volkspartij (Christen-democratische Fractie). EchoMockHandler
en de TaskCompletionSource
die zal aangeven dat de test is voltooid. Dit is belangrijk om de context terug te brengen naar de belangrijkste testdraad, zodat we fouten en time-outs correct kunnen vastleggen.
De async (message, token) => {}
Is de functie die we doorgeven in onze schijnverzorger die we hierboven noemden. Hierin kunnen we het verzoek checken en een reactie teruggeven (die we in dit geval echt niet doen).
Onze EchoMockHandler.ResponseHandler
is een helper methode die het verzoek lichaam terug naar onze methode, dit laat ons controleren of het bericht door de UmamiClient
aan de HttpClient
Juist.
public static async Task<HttpResponseMessage> ResponseHandler(HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Read the request content
var requestBody = request.Content?.ReadAsStringAsync(cancellationToken).Result;
// Create a response that echoes the request body
var responseContent = requestBody ?? "No request body";
// Return the response
return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(responseContent, Encoding.UTF8, "application/json")
});
}
We grijpen dan deze reactie en deserializeren het in een EchoedRequest
object. Dit is een eenvoudig object dat het verzoek vertegenwoordigt dat we naar de server hebben gestuurd.
public class EchoedRequest
{
public string Type { get; set; }
public UmamiPayload Payload { get; set; }
}
U ziet dit inkapselt de Type
en Payload
van het verzoek. Dit is wat we zullen controleren in onze test.
Assert.Contains("api/send", message.RequestUri.ToString());
Assert.NotNull(jsonContent);
Assert.Equal(page, jsonContent.Payload.Url);
Assert.Equal(title, jsonContent.Payload.Title);
Wat hier van cruciaal belang is, is hoe we met falende tests omgaan, omdat we niet in de hoofdcontext zitten die we moeten gebruiken. TaskCompletionSource
om terug te geven aan de hoofddraad dat de test is mislukt.
catch (Exception e)
{
tcs.SetException(e);
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
Dit zal de uitzondering op de TaskCompletionSource
en breng een 500 fout terug naar de test.
Dus dat is de eerste van mijn meer gedetailleerde berichten, IHostedService
Het is nogal complex om te testen als het hier geen waarde teruggeeft aan de beller.