Wie du ein Unit-Test für eine Nancy-API schreibst

von Sergej Kukshaus vor einem monat

Wenn ich eine API erstelle, ist dann fällt meine erste Wahl immer auf NancyFx.

Einfach deswegen, weil ich mich mittlerweile daran gewohnt habe damit zu arbeiten und weil ziemlich modular aufgebaut ist.

Und wer mich, oder mein Blog etwas näher kennt, weiß, dass ich modulare Frameworks über alles liebe 🙂

Nancy ist eigentlich nicht nur für APIs gedacht. Es ist eine smarte Alternative zu ASP.NET MVC.

Und das ist etwas, womit ich wirklich auf Kriegsfuß stehe. Ich habe es mal für ein größeres Projekt gewählt (weil ich damals noch kein Nancy kannte) und habe mir damit ein Klotz ans Bein gebunden. Ein 1000 Kilo Klotz, der es mir teilweise nicht erlaubt hat vorwärts zu kommen.

Als ich auf NancyFx gestoßen bin, war es liebe auf den ersten Blick!

Doch auch die besten Applikationen neigen zu Seiteneffekten bei Erweiterungen. Seiteneffekte, die bereits im Vorfeld zu erkennen sind.

Doch was passiert, wenn man neue Anforderungen implementiert? Man vernachlässigt die alten. Und somit wird auch die beste Applikation fehleranfällig.

API Test = GUI Test?

Frage:

Zu welcher Schicht gehört eine API?

Antwort:

Zu der UI Schicht (aus der Standalone-Schichtearchitektur).

Zu den Best Practices gehört es, keine UI Tests zu schreiben.

Wen du in deiner Anwendung UI Tests hast, dann tu dir selber ein gefallen und lösche alle deine Tests und schreibe die neu.

Sobald man nur auf die Idee kommt, ein „facelift“ zu machen, werden alle deine Tests fehlschlagen. Damit tust du dir selbst keinen Gefallen.

Viel besser ist es, die Logik-Schicht zu testen. Wenn diese stabil ist, lassen sich GUI Tests mit wenig aufwand durch manuelle Tests abbilden.

Schaut man sich jedoch eine API aus der SOA-Sicht an, gehört eine API zur Logik-Schicht und ist somit wieder für „UI-Tests“ freigegeben.

Mein steiniger Weg des Erfolges

Doch wie zur Hölle schreibst man dafür zuverlässig ein UnitTest (korrekterweise: Systemtest)?

Nach 30 Sekunden googlen, ward die Lösung schon gefunden. Das NancyFx-Team stellt extra heirfür ein Nuget-Paket inkl. Dokumentation bereit.

Dan dieser Doku, konnte ich ziemlich schnell meinen ersten Test schreiben und „Grün“ bekommen:

[Test]
public void Alive_RequestThePage_NoErrors()
{
    Task<BrowserResponse> sut = _browser.Get("/alive", ctx =>
    {
        ctx.HttpRequest();
    });

    using (new AssertionScope())
    {
        BrowserResponse response = sut.Result;

        response.StatusCode.Should().Be(HttpStatusCode.OK);
        response.Body.AsString().Should().Be("{\"alive\":\"yes\"}");
    }
}

Doch schon bald kam die Ernüchterung.

So emulierst du HttpContext

Bei meinem nächsten Test musste ich nämlich auf HttpContext.Current zugreifen.

Das blöde war, es war die ganze Zeit null.

Wenn du eine elegante Lösung dafür hast, schreibe es mir bitte in die Kommentare!

Nach stundenlanger Recherche, konnte ich schließlich das Paket FakeHttpContext finden.

Hierbei reicht es aus, eine neue Instanz der Klasse zu erstellen und schon läuft es. Einfacher, geht es wirklich kaum.

Nach einem kurzen Freudenmoment kam schon wieder die Ernüchterung!

So korrigierst du lokale Pfade

Wenn du, so wie ich, in deinen Nancy-Modulen den Konstruktor mit IRootPathProvider verwendest, wirst du dich wundern, dass der Pfad nicht mehr stimmt.

Anstelle des absoluten Pfades zur web.config, bekam ich auf einmal den absoluten Pfad zur Test.dll.

Glücklicherweise, kann unter für Nancy.Testing auch einen RootPathProvider direkt angeben.

Diesen RootPathProvider, kann man wunderbar mocken.

Nuget-Pakete und Quellcode

Für die Tests habe ich im Endeffekt folgende Pakete verwendet:

  • Nancy.Testing: Stellt Methoden bereit, um Request zu emulieren
  • NUnit: Bedarf keiner weiteren erklärung 😉 ansonsten: google.
  • FakeHttpContext: Emuliert den HttpContext
  • FluentAssertions: Alternative und lesbare Schreibweise für Assertions.
  • Moq: Sehr geiles Mocking-Framework, um einzelne Funktionen einer Implementierung zu überschreiben.
  • Newtonsoft.Json: Wird benötigt, um die Rückgabeparameter zu validieren.

Nach stundenlangem herumprobieren, ist dann folgendes entstanden:

using Moq;
using Nancy;
using Nancy.Testing;
using Newtonsoft.Json;
using NUnit.Framework;
using System.IO;
using System.Threading.Tasks;
using MockedHttpContext = FakeHttpContext.FakeHttpContext;

namespace NancyApi.Tests.Ui.ApiModuleTests
{
	[TestFixture]
	public sealed partial class ApiModuleTest
	{
		private Browser _browser;
		private MockedHttpContext _httpContext;

		[SetUp]
		public void Setup()
		{
			_httpContext = new MockedHttpContext {UserAgent = "system test"};

			var rootPathProvider = new Mock<IRootPathProvider>();
			rootPathProvider.Setup(p => p.GetRootPath()).Returns(() =>
			{
				string rootPath = Path.GetDirectoryName(typeof(ApiModuleTest).Assembly.CodeBase);

				rootPath = Path.Combine(rootPath ?? string.Empty, "..\\..\\..\\");
				rootPath = rootPath.Replace("file:\\", string.Empty);
				return rootPath;
			});

			_browser = new Browser(cfg =>
			{
				cfg.RootPathProvider(rootPathProvider.Object);
				cfg.Module<ApiModule>();
			});
		}

		[TearDown]
		public void TearDown()
		{
			_httpContext.Dispose();
		}

		private TObject JsonResponseAsObject<TObject>(Task<BrowserResponse> response)
		{
			var jsonContent = response.Result.Body.AsString();
			return JsonConvert.DeserializeObject<TObject>(jsonContent);
		}

		[Test]
		public void UpdateUser_UserAlreadyExists_UserDataChanged()
		{
			var request = new UserDataEntity
			{
				UserName = "admin",
				Password = "123456"
			};

			var browserResponse = _browser.Post("/suggestion", ctx =>
			{
				ctx.HttpRequest();
				ctx.JsonBody(request);
			});

			using (new AssertionScope())
			{
				var user = JsonResponseAsObject<UserDataEntity>(browserResponse);
				
				user.Id.Should().Be(1);
				user.UserName.Should().Be("admin");
				user.Password.Should().Be("123456");
				
			}
		}
	}
}

Fazit

Vielleicht mag es trivial und banal erscheinen, aber ich bin happy! Allein durch diese System-Tests bin ich in Zukunft sicherer bei künftigen deployements.

Ich kann per Knopfdruck überprüfen, ob die API noch so funktioniert, wie die auch funktionieren soll.

Mir ich auch schon einmal passiert, dass ich von heute auf morgen ein Parameter nicht mehr deserialisieren konnte. Es trat auch kein Fehler auf. Es passierte einfach gar nichts.

Allein durch diese Tests, kann ich in Zukunft genau solche Fehler vermeiden.

Und ich empfehle dir auch dringend, Tests für deine API zu schreiben, solltest du noch keine haben!

Sei der erste und teile deine Meinung mit der Welt!
... und was meinst du dazu?
Deine E-Mail-Adresse wird nicht veröffentlicht.