Tutorial: F# mit SAFE-Stack – Teil 5

Keine Kommentare

Eine Anwendung mit nur einer (funktionalen) Programmiersprache entwickeln

Prüfung, Test
(https://unsplash.com/photos/BCBGahg0MH0)

Willkommen zurück zum fünften und letzten Teil der Serie.

Im ersten Teil der Serie haben wir das Grundgerüst für eine sehr einfache Todo-Anwendung gebaut, die zunächst nur eine feste Liste von Aufgaben anzeigen konnte. Im zweiten Teil haben wir sie dann erweitert, um neue Aufgaben hinzufügen zu können. Am Ende des dritten Teils war die Anwendung dann bereits feature complete, existierende Aufgaben können jetzt als abgeschlossen markiert werden. Im vierten Teil haben uns um die Optik gekümmert und die Anwendung deutlich ansprechender gestaltet.

Die Web-Anwendung ist vollständig in F# geschrieben und benutzt die Bibliotheken des SAFE-Stacks, insbesondere:

  • Giraffe (für die Bearbeitung von HTTP-Requests)
  • Fable (F# nach Javascript und React-UI-Elemente)
  • Elmish (für das Model-Update-View-Pattern)

Funktional und optisch sind wir fertig. Aber einen ganz wichtigen Teil haben wir bisher unterschlagen, und zwar einen, der sehr gerne vergessen – oder sollte ich sagen: verdrängt? – wird:

Testen

Jede Anwendung muss getestet werden, bevor sie produktiv eingesetzt wird! State of the art sind dabei natürlich automatisierte Tests. Und genau diese wollen wir heute zum Einsatz bringen. Ihr werdet sehen, dass auch das sehr einfach geht. Generell spielt bei automatisiertem Testen der funktionale Programmierstil seine Stärke voll aus.

Test-Frameworks

Für .NET gibt es mehrere Test-Frameworks, aus denen ihr wählen könnt. Für die Verwendung mit F# habe ich diese hier gefunden:

Zu allen Frameworks gibt es Dokumentation von Microsoft selbst aber auch aus anderen Quellen.

MSTest scheint veraltet zu sein. NUnit ist sehr deutlich von JUnit abgeleitet, das für Java und andere Sprachen der Java Virtual Machine den de facto-Standard darstellt. xUnit ist eine Weiterentwicklung von NUnit.

Für dieses Tutorial habe ich mich für xUnit entschieden. Es scheint mir für unseren Anwendungsfall am einfachsten und klarsten einsetzbar zu sein, außerdem ist es das neueste der drei Frameworks. Insbesondere können wir mit xUnit einfache Testfunktionen schreiben, so wie es sich für F# gehört. Mit den anderen Bibliotheken müssen wir eine Klasse erzeugen, da dies bei C#, der Hauptsprache von .NET, so gemacht wird. Geht zwar in F#, passt aber nicht zum funktionalen Paradigma.

Also nehmen wir xUnit.

Den vollständigen Code vom letzten Mal findet ihr hier.

Den Code von heute gibt es hier.

Ein Testprojekt hinzufügen

Aktuell befinden sich in unserem Projektverzeichnis drei .NET-Projekte, die jeweils in einem eigenen Verzeichnis untergebracht sind:

  • src/Client
  • src/Shared
  • src/Server

Jetzt fügen wir noch ein viertes hinzu, in dem die Tests angesiedelt werden.

Dazu erzeugen wir ein neues Verzeichnis und wechseln hinein:

cd src
mkdir Test
cd Test

In diesem Verzeichnis erzeugen wir jetzt ein Projekt für xUnit:

dotnet new xunit -lang "F#"

Will man eines der anderen Frameworks verwenden, muss man xunit durch nunit oder mstest austauschen.

Jetzt müssen wir noch diejenigen Projekte hinzufügen, deren Code wir testen möchten, bzw. die davon abhängig sind:

dotnet add reference ../Shared/Shared.fsproj
dotnet add reference ../Server/Server.fsproj

Ab jetzt ist alles klar und wir können starten, die Tests zu schreiben.

Test.fs

In dieser Datei stehen unsere Tests. Als Vorlage ist diese Datei schon angelegt. Im oberen Bereich »öffnen« wir die Module, auf die wir uns beziehen möchten:

open Xunit

open Shared
open Server

Jede andere Funktion, die noch in der Datei steht, können wir löschen.

Jetzt können wir unsere Testfunktionen schreiben. Damit sie vom xUnit auch erkannt werden, verstehen wir sie mit der Annotation [<Fact>].

Wir werden nur unser Todo-Modul testen. In diesem Modul steckt die »Business-Logik« unserer Anwendung. Natürlich kann man auch die anderen Funktionen testen, aber für dieses Tutorial reicht mir das.

Fangen wir also damit an, den Mechanismus zu testen, der neue Ids für die Aufgaben erzeugt:

[<Fact>]
let ``create new Id`` () =
    let model = [{ Id = 2; Description = "Todo 1"; Completed = false}]
    let result = Todos.newId model
    Assert.Equal(3, result)

Wir erzeugen ein Model mit einem Todo mit der Id 2 und erwarten, dass uns die 3 als neue, mögliche Id zurückgegeben wird.

Die doppelten Backticks `` erlaubt es, in F# Bezeichner mit Leerzeichen zu verwenden. Im normalen Code ist das sicher keine gute Idee, da Leerzeichen dort eine andere Bedeutung haben und zu Verwirrung führen können. Aber für die Bezeichnung eines Tests sind sprechende Namen ungemein empfehlenswert, und wir können auf Unterstriche oder CamelCase verzichten.

Wie schon gesagt, spielt das funktionale Paradigma beim Testen seine Stärke aus. Da das Ergebnis einer pure function (wie man sie möglichst verwenden sollte) nur von den Eingabeparametern abhängt, muss man nur diese richtig erzeugen und die Funktion aufrufen. Dadurch wird das given-when-then-Pattern eines Tests deutlich sichtbar:

given => let model = [{ Id = 2;
                        Description = "Todo 1"; Completed = false}]
when  => let result = Todos.newId model
then  => Assert.Equal(3, result)

Der größte Aufwand besteht darin, die Eingabewerte und das erwartete Ergebnis zu konstruieren. Dazwischen steht dann immer nur der Funktionsaufruf der zu testenden Funktion

So können wir auch die anderen beiden Funktionalitäten unserer Geschäftslogik testen:

[<Fact>]
let ``add todo`` () =
    let model = [{ Id = 1; Description = "Todo 1"; Completed = false}]
    let result = Todos.addTodo model "Todo 2"
    let expected = { Id = 2; Description = "Todo 2"; Completed = false}
    Assert.Contains(model.Head, result)
    Assert.Contains(expected, result)

[<Fact>]
let ``shall toggle complete state`` () =
    let model = [{ Id = 1; Description = "Description"; Completed = false}]
    let result = Todos.toggleComplete model 1
    let expected = [{ Id = 1; Description = "Description"; Completed = true}]
    Assert.Equal<Todo list>(expected, result)

Im ersten Fall haben wir die Existenz des alten und des neuen Todos im Model geprüft, beim zweiten Mal sogar das exakte Model angegeben, das wir durch die Transformation erwarten.

Warum wir beim zweiten Test in der Assert.Equal-Methode den Typ angeben müssen, weiß ich nicht, aber ohne diese Annotation <Todo list> kommt der Compiler nicht klar.

Diese Tests sind sicher nicht ausreichend. Insbesondere werden keine etch-cases getestet (leere Listen, Dubletten etc.) Aber wie bei allem in diesem Tutorial geht es ja nur um das Prinzip.

Was ist mit mocking?

Ein großes Thema beim Testen ist normalerweise das mocking. Dabei werden diejenigen Objekte und deren Funktionalitäten, die die zu testende Funktion benötigt, durch Fassaden-Objekte ersetzt, deren Verhalten man kontrolliert und später abfragen kann. Geht das auch in F#?

Grundsätzlich ja. Mit Foq existiert auch eine entsprechende Bibliothek für F#.

Doch im großen und ganzen benötigt man mocking bei funktionalen Sprachen nicht. Beziehungsweise, man benötigt dafür kein Framework.

Wieso?

Weil die Funktionen, die man testet keine Seiteneffekte haben – zumindest meistens. Wenn einer Funktion eine bestimmte Funktionalität dynamisch zur Verfügung gestellt werden soll, dann gibt man diese als Parameter mit. Innerhalb eines Tests kann man leicht ein Lambda so erstellen, dass es das gewünschte Verhalten hat, und der zu testenden Funktion mitgeben.

Es gibt keine Referenzen, die Objekte auf andere Objekte halten und die man mocken müsste. Das Ergebnis einer Funktion hängt nur von den Eingabeparametern an. Und über die habe ich in meiner Test-Funktion die volle Kontrolle.

So einfach ist das.

Zusammenfassung

Damit sind wir am Ende unserer Artikelserie angekommen. Ich hoffe, sie hat euch Freude gemacht, und ihr konntet etwas für eure zukünftigen Projekte mitnehmen.

Der SAFE-Stack ist eine mächtige Sammlung von einfach zu nutzenden Frameworks, mit denen man full stack Anwendungen schreiben kann.

Aber natürlich kann man die Komponenten auch einzeln verwenden. Mithilfe von Saturn kann man funktionale Web-Backends schreiben, losgelöst vom Frontend.

Fable und Elmish ermöglichen es, Web-Frontends in F# zu schreiben, die mit sehr wenig Overhead ansprechende und funktionale Oberfläche bieten. Der F#-Code kann dabei problemlos mit anderem, in JavaScript geschriebenem Code interagieren.

Sprich: es muss nicht immer full stack sein. Ihr könnt die Komponenten auch einzeln verwenden.

Ich habe bei allen Frameworks immer nur die Grundlagen gezeigt. Alle können noch viel mehr. So ermöglicht es Saturn zum Beispiel, mithilfe eines controller alle HTTP-Endpunkte für CRUD-Operationen in REST-konformer Konvention zu erstellen, ohne die Pfade und HTTP-Verben einzeln definieren zu müssen (siehe hier).

Elmish unterstützt unter anderem die Manipulation der history eines Browsers, um Usern Bookmarks auf einzelne Dialoge der Anwendung zu ermöglichen (siehe hier).

Es gibt noch Vieles zu entdecken.

Und dabei wünsche ich euch viel Spaß.

Goetz Markgraf

Goetz hat Wirtschaftsinformatik studiert und viele Jahre als Softwareentwickler und Project Manager gearbeitet. Sein Fokus liegt dabei immer auf dem Verständnis für die Situation und Herausforderungen der Kunden und darin, dies für Entwickler verständlich zu machen.
Seit 2018 ist er Consultant bei der codecentric AG.

Über 1.000 Abonnenten sind up to date!

Die neuesten Tipps, Tricks, Tools und Technologien. Jede Woche direkt in deine Inbox.

Kostenfrei anmelden und immer auf dem neuesten Stand bleiben!
(Keine Sorge, du kannst dich jederzeit abmelden.)

* Hiermit willige ich in die Erhebung und Verarbeitung der vorstehenden Daten für das Empfangen des monatlichen Newsletters der codecentric AG per E-Mail ein. Ihre Einwilligung können Sie per E-Mail an datenschutz@codecentric.de, in der Informations-E-Mail selbst per Link oder an die im Impressum genannten Kontaktdaten jederzeit widerrufen. Von der Datenschutzerklärung der codecentric AG habe ich Kenntnis genommen und bestätige dies mit Absendung des Formulars.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.