Testing @ Novaloop

Wir schreiben gerne Tests und das meinen wir ernst.

Damit das für alle von uns so bleibt benutzen wir einen modernen Test Stack und vereinfachen unseren Testalltag soweit wie möglich. Dazu gehört auch, dass wir zu unseren Projekten keine Mehrseitigen Dokumentationen zum Test Setup mitliefern müssen. Wie machen wir das? So!

Unsere Philosophie des Testens

Wenn wir Tests schreiben dann folgenden wir diesen Grundsätzen:

  • Integration Tests für alle guten Pfade, möglichst nah an der produktiven Umgebung
  • Unit Tests für komplexere Module, aber nicht zwingend eine Test Coverage von 100%
  • Unit-of-work testing über striktem Methoden Testing
  • Nur so viele Mocks wie nötig, aber Code, der Mocks erlaubt
  • kleine Änderungen im Code sollen möglichst wenig Anpassungen an den Tests verursachen, Code sollte Easy-to-change sein
  • Test-driven development wo möglich und sinnvoll

Libraries, die in jedes Novaloop Testprojekt gehören

Xunit

Als Testdriver setzen wir auf Xunit. Es wird aktiv entwickelt und lässt sich mit der aktuellsten .NET Plattform benutzen.

Fluent Assertions

Fluent Assertions ermöglichen uns die Tests lesbar zu schreiben und drücken die Intention der Autorin oder des Autors klar aus.

Verify

So schön Fluent Assertions auch sind, wir wollen es nicht übertreiben. Auch der Vergleich von ganzen JSON Dokumenten ist eher aufwändig mit herkömlichen Methoden. Hier schafft Snapshot Testing abhilfe. Wir benutzen dazu Verify.

Es gibt sogar ein Plugin für Jetbrains basierte IDEs.

FluentDocker

Wir möchten unserem Projekt keine mehrseiten Dokumentation für das Test Setup beilegen wollen. Für ein einfaches On-Boarding von neuen Entwickler*innen haben wir uns für FluentDocker entschieden.

Fluent Assertions

Fluent Assertions erlaubt uns elegante Assertions zu schreiben, die auch die Motivation für die Assertion klar zum Ausdruck bringen.

Ein Beispiel dazu findet man auf der Website:


string actual = "ABCDEFGHI";
actual.Should()
    .StartWith("AB")
    .And.EndWith("HI")
    .And.Contain("EF")
    .And.HaveLength(9);

Die Intention lässt sich auch in einer allfälligen Fehlermeldung für die Assertion klar hinterlegen:


IEnumerable<int> numbers = new[] { 1, 2, 3 };

numbers.Should().OnlyContain(n => n > 0); numbers.Should().HaveCount(4, "because we thought we put four items in the collection");

Ausserdem produziert Fluent Assertions sinnvolle und hilfreiche Fehlermeldungen:


string username = "dennis";
username.Should().Be("jonas");

Die Fehlermeldung sieht dann folgendermassen aus:


Expected username to be "jonas" with a length of 5, 
but "dennis" has a length of 6, differs near "den" (index 0).

Verify

Wenn man ganze Objekte oder JSON Strings miteinander vergleichen möchte, macht es solten Sinn dies mit einer Assertion Library zu machen. Hier helfen sogenannte Snapshot Erweiterungen weiter. Das Prinzip ist einfach.

  1. Test laufen lassen
  2. das Resultat vom Test speichern und analysieren
  3. wenn das Resultat den Erwartungen entspricht, wird es als erwartetes Ergebnis markiert und ins Repo eingecheckt
  4. bei den nächsten Testläufen wird das Resultat mit dem akzeptierten Snapshot verglichen

Einige Proprties sind dynmaisch und lassen sich nicht über Snapshots testen. Nehmen wir an, dass wir die Id für ein Objekt fortlaufend generieren. Die Verify Library bietet uns dazu ein Settings Objekt an:


var myId = 123;
var myObject = new MyObject { Id = 123 };
var verifySettings = new VerifySettings();
var verifySettings.ModifySerialization(_ =>
{
    _.IgnoreMember("Id");
});
...
<Do something important>
...
await Verifier.Verify(myObject, _verifySettings);
myObject.Id.Should.Be(myId);

Wenn dann Änderungen im Code gemacht werden, wiederholt man die Schritte 2, 3 und 4 und schon sind die Tests wieder auf dem neusten Stand.

Fluent Docker

Für End-To-End Tests verwenden wir Fluent Docker.. So können die Tests ohne Aufwand auf beliebigen Servern (CI / CD) und Computern (Entwickler*innen) ausgeführt werden.

Dazu legen wir eine docker-compose.yml Datei an. Darin werden alle nötigen Services und abhängigkeiten definiert. Diese können dann vor dem effektiven Test Durchlauf hoch- und danach wieder runtergefahren werden.


var dockerComposeFile = Path.Combine(Directory.GetCurrentDirectory(), 
    "docker-compose.yaml");
var hosts = new Hosts().Discover();
var dockerHost = hosts.FirstOrDefault(x => x.IsNative) ?? 
    hosts.FirstOrDefault(x => x.Name == "default");

Service = new DockerComposeCompositeService(dockerHost, new DockerComposeConfig { ComposeFilePath = new List<string> { dockerComposeFile }, ForceRecreate = true, RemoveOrphans = true, StopOnDispose = true, } );

Service.Start(); Assert.Equal(ServiceRunningState.Running, Service.State); Assert.Equal(5, Service.Containers.Count); Condition.Await(() => Service.Containers .Any(c => c.State != ServiceRunningState.Running), 30 * 1000);

Interessiert? Möchten Sie mehr erfahren?

Haben wir Ihr Interesse geweckt? Möchten Sie mehr erfahren?

Wir freuen uns auf Sie!