Blog
Mocking in der realen Welt! Testen von HTTP-Diensten in C# mit Wiremock.NET

Das Schreiben von Tests ist an sich schon schwierig. Oft wird vergessen, dass es verschiedene Arten von Tests braucht. Es gibt Unit-Tests, die eine bestimmte Funktionalität abdecken, und diese Tests haben einen klaren Umfang. Es gibt Systemtests, die eine Reihe von Funktionen abdecken, aber diese Tests ersetzen die externen Systeme durch unechte Systeme. Es gibt auch Integrationstests, die wie Systemtests ablaufen, aber in diesem Fall sind externe Systeme in den Testprozess eingebunden.
Die Pflege von Integrationstests ist der schwierigste Teil, wenn es um die Pflege von Tests geht. Es gibt eine Menge Abhängigkeiten, die nicht unbedingt unter Ihrer Kontrolle stehen. Außerdem ist es nicht einfach, die Integrationstests in die CI-Pipeline einzubinden. Jede Art von Test hat ihre eigenen Schwierigkeiten. In diesem Artikel werden wir uns mit den System- und Integrationstests befassen. WireMock.Net wird eine wichtige Rolle bei der Umwandlung der schwer zu wartenden Integrationstests in kontrollierbare Systemtests spielen und umgekehrt.
Was ist WireMock.Net?
WireMock.Net ist ein GitHub-Community-Projekt[1] und enthält die C#-Implementierung von mock4net, die die Funktionalität des JAVA-basierten WireMock.org nachahmt. Die Idee hinter WireMock.Net ist es, das Verhalten einer realen HTTP-API zu imitieren. HTTP-Anfragen, die von dem zu testenden Code ausgehen, werden erfasst und an einen WireMock.Net HTTP-Server (der Teil des Test-Frameworks ist) gesendet. Als Ergebnis wird eine HTTP-Antwort zurückgegeben, die anhand eines erwarteten Verhaltens überprüft werden kann.
Welche Funktionen bietet WireMock.Net?
Eine der interessantesten Funktionen von WireMock.Net ist, dass es in Testprojekten verwendet werden kann. Es kann erfasste Nachrichten aufzeichnen und wiedergeben. In Integrationstests kann WireMock.Net so eingerichtet werden, dass es als Proxy fungiert, um die HTTP-Anfragen zu erfassen und/oder weiterzuleiten. WireMock.Net kann auch so konfiguriert werden, dass es die passende Antwort gibt, wenn es ähnliche Anfragen sieht. Das bedeutet, dass Integrationstests in Systemtests umgewandelt werden können und andersherum. Natürlich können für diese eingehenden Anfragen auch Assertions geschrieben werden.
Wenn die Anfragen zu dynamisch sind, ist der Abgleich von Anfragen die Technik, die Sie verwenden sollten, um eingehende Anfragen zu verallgemeinern. Anfragen können anhand von URL, Pfad, Anfragemethode, Anfragekopf, Cookies und/oder Anfragebody abgeglichen werden.
WireMock.Net kann auch verwendet werden, wenn manuelle Tests erforderlich sind, aber die Abhängigkeiten nicht bereit oder in der Testumgebung nicht verfügbar sind. Es kann als eigenständiges Tool ausgeführt werden.
Hallo Welt!
Da wir nun wissen, worum es bei WireMock.Net geht, sehen wir uns an, wie es mit XUnit eingerichtet werden kann. Das folgende Beispiel beschreibt eine einfache Testmethode, die einen wiremock.net Server einrichtet. Sie definiert eine Anfrage und eine Antwort. Wenn eine Get-Anfrage mit dem Pfad "/foo" an den WireMockserver gesendet wird, wird eine Antwort mit dem Statuscode OK und dem Inhalt "bar" erstellt.
public class GivenAWireMockServer
{
[Fact]
[Trait("Category","SystemTests")]
public async Task WhenSendingAGetRequestTo_foo_ReceiveAResponse_bar()
{
//Arrange
var wireMockServer = WireMock.Server.WireMockServer.Start();
wireMockServer.Given(Request.Create()
.UsingGet()
.WithPath("/hello")
).RespondWith(Response.Create()
.WithStatusCode(HttpStatusCode.OK)
.WithBody("world"));
var httpClient = wireMockServer.CreateClient();
//Act
var barResponse = await httpClient.GetAsync("hello");
var body = await barResponse.Content.ReadAsStringAsync();
//Assert
Assert.Equal(HttpStatusCode.OK, barResponse.StatusCode);
Assert.Equal("world", body);
}
}
Das obige Szenario soll natürlich nur zeigen, wie einfach es ist, einen Test einzurichten. Sehen wir uns an, wie er als Integrationstest verwendet werden kann.
Test-Szenario
Der folgende Code stellt einen WeatherForecastController mit einer Get-Methode dar. Wenn Sie http://localhost:3011/ weatherforecast aufrufen, erhalten Sie eine der folgenden Meldungen: "Zu heiß", "Gemütlich", "Kalt" oder "Zu kalt".
Die Methode konsumiert einen Wettervorhersagedienst https://api.open-meteo.com/v1/forecast. Um diesen Dienst zu nutzen, ist der WeatherForecastController von IOpenMeteoClient abhängig, der wiederum von der IHttpClientFactory abhängt.
Die httpClientFactory erstellt einen HttpClient, der die Prognose vom Wettervorhersagedienst abruft. Auf der Grundlage des Ergebnisses gibt die Methode im WeatherForecastController eine der oben definierten Nachrichten zurück. Der OpenMeteoClient ist eigentlich ein Proxy, der hilft, die Tatsache zu verbergen, dass eine HttpClientFactory verwendet wird.
Definieren Sie den ersten Integrationstest
Bevor ein Integrationstest definiert werden kann, muss man wissen, welche Nachrichten und Antworten über die Leitung hin und her gehen. Fiddler kann verwendet werden, um den Datenverkehr zwischen dem WeatherForecastController und der open-meteo api abzufangen. Die eigentliche Anfrage und die Antwort sehen wie folgt aus:
anfordern.
GET https://api.open-meteo.com/v1/forecast?latitude=51.09&longitude=4.06&daily=temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=Europe%2FBerlin&start_date=2022-07-31&end_date=2022-07-31 HTTP/1.1
Host: api.open-meteo.com
traceparent: 00-1e25de5aeaa91dba70b47f8679ea7dc9-d74d7ed281f40bdb-00
Antwort
HTTP/1.1 200 OK
Date: Sun, 31 Jul 2022 15:13:41 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
{“latitude”:53.1,”longitude”:5.06,”generationtime_ms”:0.38301944732666016,”utc_offset_seconds”:7200,”elevation”:3.0,”current_weather”:{“temperature”:22.5,”windspeed”:22.8,”winddirection”:249.0,”weathercode”:3.0,”time”:”2022-07-31T17:00”},”daily_units”:{“time”:”iso8601”,”temperature_2m_max”:”°C”,”temperature_2m_min”:”°C”},”daily”:{“time”:[“2022-07-31”],”temperature_2m_max”:[23.6],”temperature_2m_min”:[17.0]}}
Wenn Sie WireMock.Net für den Integrationstest verwenden, wird eine gespiegelte IhttpClientFactory eingerichtet, die den HttpClient des eingebetteten WireMock.Net Servers zurückgibt. Der WireMock.Net Server läuft im selben Prozess, ist aber auch von "außen" erreichbar.
Ein einfacher Integrationstest der Get-Methode des WeatherForecastControllers aus dem Testszenario kann wie folgt aussehen:
[Fact]
[Trait("Category","IntegrationTests")]
public async Task WhenRequestingCurrentWeatherInformation_
DateShouldBeUtcToday()
{
//Arrange
var application = new WebApplicationFactory()
.WithWebHostBuilder(builder >
{
builder.ConfigureServices(
services > services.AddConfiguredServices());
});
//Act
var httpClient = application.CreateClient();
//Assert
var response = await httpClient.GetAsync(“/WeatherForecast”);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var weather = await response.Content.ReadFromJsonAsync
();
...
}
Dieser Integrationstest prüft, ob eine ausgefüllte WeatherForecast-Antwort von der API zurückgegeben wurde und ob das Datum der Wettervorhersage mit dem Datum des lokalen Systems übereinstimmt.
Integrationstest: Verwendung des Request Matchers
Der folgende Code zeigt, wie Sie den Abgleich der Anfrage einrichten und die entsprechende Antwort definieren. Der Abgleich ist recht allgemein gehalten, kann aber so streng wie gewünscht eingestellt werden.
public async Task WhenRequestingCurrentWeatherInformation_
DateShouldBeUtcToday()
{
//Arrange
var openMeteoWireMockServer = WireMock.Server.WireMockServer.Start();
openMeteoWireMockServer.Given(Request.Create()
.UsingGet()
.WithPath(path > path.Contains("forecast"))
).RespondWith(Response.Create()
.WithStatusCode(HttpStatusCode.OK)
.WithBody(..."current_weather":{"temperature
":22.5,"windspeed":22.8,"winddirection":249.0,
"weathercode":3.0,"time":"2022-07-31T17:00"...));
var openMeteoHttpClient = openMeteoWireMockServer.
CreateClient();
var fakeHttpClientFactory = new Fake( );
fakeHttpClientFactory.CallsTo(httpClientFactory >
httpClientFactory.CreateClient(“OpenMeteo”))
.Returns(openMeteoHttpClient);
var application = new WebApplicationFactory()
.WithWebHostBuilder(builder >
{
builder.ConfigureServices(
services >
{
services.AddConfiguredServices();
services.AddScoped(provider >
fakeHttpClientFactory.FakedObject);
});
});
//Act
var httpClient = application.CreateClient();
//Assert
var response = await httpClient.GetAsync("/WeatherForecast");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var weather = await response.Content.ReadFromJsonAsync
();
...
}
Öffnen Sie Fiddler, führen Sie den Test aus und stellen Sie fest, dass es einen Aufruf vom WeatherForcastController an die open-meteo api gibt. Anstatt auf den
Integrationstest: Arbeiten mit aufgezeichneten Nachrichten
Es wird ein neuer Test eingerichtet, bei dem aufgezeichnete Nachrichten verwendet werden. In diesem Szenario gibt WireMock.Net diese Nachrichten wieder, wenn die entsprechende Anfrage eingeht. Fiddler wird als Proxy-Server verwendet, um zu überwachen, was vor sich geht.
Um den Test für die Arbeit mit aufgezeichneten Nachrichten vorzubereiten, müssen die Antwortnachrichten zunächst aufgezeichnet werden. Der folgende Code zeigt, wie Sie die Nachrichten aufzeichnen. Die aufgezeichneten Nachrichten werden im lokalen Debug-Ordner gespeichert.
//Arrange
var openMeteoWireMockServer = WireMock.Server.WireMockServer.Start(
new WireMockServerSettings()
{
ProxyAndRecordSettings = new ProxyAndRecordSettings()
{
Url = "https://api.open-meteo.com",
SaveMapping = true,
SaveMappingToFile = true,
WebProxySettings = new WebProxySettings()
{
Address = "127.0.0.1:8888"
},
ExcludedHeaders = new string[]{"Host", "traceparent"
}
},
StartAdminInterface = true,
FileSystemHandler = new LocalFileSystemHandler(".")
});
var openMeteoHttpClient = openMeteoWireMockServer.
CreateClient();
Öffnen Sie Fiddler und führen Sie den Test aus. Beachten Sie, dass ein tatsächlicher Aufruf an die open-meteo API weitergeleitet wird.
Die Mappings werden im Debug-Ordner des Projekts aufgezeichnet, wie unten gezeigt.
Nach der anfänglichen Aufzeichnung kann der Wiedergabemechanismus verwendet werden, wie im folgenden Beispiel gezeigt. Für fortgeschrittene Anwendungsfälle kann das Mapping-Modell geöffnet, geteilt oder organisiert werden. Im folgenden Beispiel verarbeitet WireMock.Net das Mapping-Modell so, wie es aufgezeichnet wurde
//Arrange
var openMeteoWireMockServer = WireMock.Server.
WireMockServer.Start(
new WireMockServerSettings()
{
ReadStaticMappings = true
});
//Act
var openMeteoHttpClient = openMeteoWireMockServer.
CreateClient();
...
//Assert
...
WireMock.Net in einer CI-Pipeline verwenden
WireMock.Net kann auch für automatisierte Tests in GitHub Workflow-Aktionen verwendet werden. In dem folgenden Pipeline-Beispiel wird eine einfache GitHub-Workflow-Aktion eingerichtet, die den Code wiederherstellt, erstellt und testet. Die folgende yaml-Datei ist ein Beispiel.
name: .NET
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore ./src/WiremockSamples.sln
- name: Build
run: dotnet build ./src/WiremockSamples.sln
-no-restore
- name: Test
run: dotnet test ./src/WiremockSamples.sln -nobuild -verbosity normal -filter "Category=SystemTests"
Die Tests haben das Attribut Trait mit dem Schlüssel "Category" und dem Wert "IntegrationTests" oder "SystemTests". Wenn alle Tests ausgeführt werden, schlägt die Pipeline fehl, weil die Integrationstests versuchen, die eigentliche open-meteo-API zu kontaktieren. Diese Api kann vom CI-Server aus nicht erreicht werden. Um dies zu umgehen, wird ein Filter angewendet, der sicherstellt, dass nur "SystemTests" ausgeführt werden. Lassen Sie uns sehen, was passiert, wenn die Pipeline ausgeführt wurde. Die Ergebnisse der Pipeline zeigen, dass der Systemtest erfolgreich ausgeführt wurde.
Zusammenfassung
Dieser Artikel hat nur an der Oberfläche gekratzt, wenn es um das Testen mit WinMock.Net geht. Es gibt noch viel mehr mit WireMock.Net zu entdecken, aber das oben Gesagte wird Ihnen helfen, Integrations- und Systemtests in jedem .NET-Projekt einzurichten, das Http-basierte Dienste verwendet.
Die WireMock.Net Wiki[2] Seiten auf GitHub sind selbsterklärend. Das Repository enthält jede Menge Beispiele, in denen die gängigsten Szenarien abgedeckt sind. Die einfache Möglichkeit, Http-basierte Dienste mit echten Anfragen und Antworten zu testen, macht es lohnenswert, bestehende Unit-Tests neu zu bewerten. Anstatt ganze Funktionen zu mocken, können nur die internen Http-Anfragen und -Antworten gemockt werden, was eine zusätzliche Ebene der Kontrolle im Softwaretestprozess bietet. Die Quellen in diesem Artikel sind ebenfalls auf GitHub zu finden[3].
[1] github.com/WireMock-Net/WireMock.Net
Verfasst von

Bas van de Sande
Azure Coding Architect | Consultant | Integrator | Trainer A person who never made a mistake never tried anything new. – Albert Einstein
Contact