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
Im vorigen Artikel haben wir diskutiert, wie man die UmamiClient
Verwendung von xUnit und Moq. In diesem Artikel werden wir diskutieren, wie man die UmamiBackgroundSender
Unterricht. Das UmamiBackgroundSender
ist ein bisschen anders als UmamiClient
wie es verwendet IHostedService
um im Hintergrund zu bleiben und Anfragen durch zu senden UmamiClient
komplett aus dem Hauptausführungsgewinde heraus (es blockiert also nicht die Ausführung).
Wie immer kannst du den Quellcode dazu auf meinem GitHub sehen. Hierher.
UmamiBackgroundSender
Die tatsächliche Struktur der UmamiBackgroundSender
ist ganz einfach. Es ist ein gehosteter Dienst, der Anfragen an den Umami-Server sendet, sobald er eine neue Anfrage erkennt. Die Grundstruktur UmamiBackgroundSender
Die Klasse ist unten dargestellt:
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);
}
Wie Sie sehen können, ist dies nur ein Klassiker IHostedService
es wird zu unserer Service-Sammlung in ASP.NET mit dem hinzugefügt services.AddHostedService<UmamiBackgroundSender>()
verfahren. Dies beginnt mit der StartAsync
Methode, wenn die Anwendung beginnt.
Der Blick in den SendRequest
Methode ist, wo die Magie geschieht. Hier lesen wir vom Kanal aus und senden die Anfrage an den Umami-Server.
Dies schließt die eigentlichen Methoden zum Senden der Anfragen aus (siehe unten).
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")
Alles, was diese wirklich tun, ist die Anfrage in die SendBackgroundPayload
Nehmen Sie es auf und senden Sie es an den Kanal.
Unsere verschachtelten empfangen Schleife in SendRequest
wird vom Kanal lesen, bis es geschlossen ist. Hier werden wir unsere Testbemühungen fokussieren.
while (await _channel.Reader.WaitToReadAsync(token))
{
while (_channel.Reader.TryRead(out var payload))
{
}
}
Der Hintergrunddienst hat einige Semantik, die es erlauben, einfach die Nachricht abzufeuern, sobald sie kommt.
Dies wirft jedoch ein Problem auf; wenn wir nicht einen zurückgegebenen Wert von der Send
Wie testen wir, dass dies tatsächlich etwas tut?
UmamiBackgroundSender
Die Frage ist also, wie wir diesen Dienst 5n testen, da gibt es keine Antwort, gegen den wir eigentlich testen?
Die Antwort lautet: HttpMessageHandler
an den verspotteten HttpClient, den wir in unseren UmamiClient schicken. Dadurch können wir die Anfrage abfangen und den Inhalt überprüfen.
Sie werden sich an den vorherigen Artikel erinnern, den wir einen HttpMessageHandler eingerichtet haben. Das Leben in der 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;
}
Sie können hier sehen, wir verwenden Mock, um eine SendAsync
Methode, die eine Antwort basierend auf der Anfrage zurückgibt (in HttpClient werden alle async-Anfragen durchgeführt durch SendAsync
).
Sehen Sie, wir haben zuerst den Mock eingerichtet.
var mockHandler = new Mock<HttpMessageHandler>();
Wir benutzen dann die Magie von Protected
zur Einrichtung der SendAsync
verfahren. Das ist, weil SendAsync
ist normalerweise nicht zugänglich in der öffentlichen API von HttpMessageHandler
.
public abstract class HttpMessageHandler : IDisposable
{
protected HttpMessageHandler()
{
}
protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
Wir benutzen dann einfach den Fang-all ItExpr.IsAny
um jede Anfrage zu erfüllen und die Antwort von der responseFunc
Wir gehen rein.
Im Inneren des UmamiBackgroundSender_Tests
Klasse haben wir einen gemeinsamen Weg, um alle Testmethoden zu definieren.
[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);
}
Wenn wir dies einmal definiert haben, müssen wir unsere IHostedService
Lebensdauer der Prüfmethode:
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);
}
Sie können sehen, dass wir im Handler zu unserem GetServices
Einrichtungsmethode:
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 gehen wir in unserem Handler zu unseren Dienstleistungen, um es in der UmamiClient
Einrichtung.
Wir fügen dann die UmamiBackgroundSender
zur Service-Sammlung und erhalten Sie die IHostedService
vom Dienstleister. Dann geben Sie dies in die Testklasse zurück, um die Verwendung zu erlauben.
Nun, da wir alle diese Einrichtungen haben, können wir einfach StartAsync
der Hosted Service, verwenden Sie es dann warten, bis es stoppt:
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);
Dies startet den gehosteten Service, sendet die Anfrage, wartet auf die Antwort und stoppt den Service.
Wir beginnen zuerst mit der Einrichtung der EchoMockHandler
und der TaskCompletionSource
die das Signal geben, dass der Test abgeschlossen ist. Dies ist wichtig, um den Kontext zum Haupttest Thread zurückzugeben, damit wir Fehler und Timeouts korrekt erfassen können.
Das async (message, token) => {}
ist die Funktion, die wir in unseren mock handler übergeben, die wir oben erwähnt. Hier können wir die Anfrage überprüfen und eine Antwort zurückgeben (was wir in diesem Fall wirklich nicht tun).
Unsere EchoMockHandler.ResponseHandler
ist ein Helfer-Methode, die die Anfrage Körper zurück zu unserer Methode, so können wir überprüfen, die Nachricht wird durch die UmamiClient
zu dem HttpClient
Richtig.
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")
});
}
Wir greifen dann diese Antwort und deserialisieren sie in eine EchoedRequest
Gegenstand. Dies ist ein einfaches Objekt, das die Anfrage darstellt, die wir an den Server gesendet haben.
public class EchoedRequest
{
public string Type { get; set; }
public UmamiPayload Payload { get; set; }
}
Sie sehen, dass dies verkapselt die Type
und Payload
des Antrags. Darauf werden wir in unserem Test achten.
Assert.Contains("api/send", message.RequestUri.ToString());
Assert.NotNull(jsonContent);
Assert.Equal(page, jsonContent.Payload.Url);
Assert.Equal(title, jsonContent.Payload.Title);
Was hier kritisch ist, ist, wie wir mit Fehlertests umgehen, da wir nicht im Haupt-Thread-Kontext hier sind, müssen wir verwenden TaskCompletionSource
zurück zum Hauptgewinde zu signalisieren, dass der Test fehlgeschlagen ist.
catch (Exception e)
{
tcs.SetException(e);
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
Dies setzt die Ausnahme auf die TaskCompletionSource
und einen 500-Fehler an den Test zurückgeben.
Also das ist der erste meiner eher detaillierteren Beiträge, IHostedService
Das rechtfertigt, da es ziemlich komplex ist, zu testen, wenn es wie hier keinen Wert an den Anrufer zurückgibt.