Unit Tests ohne Frust mit der SharePoint-Framework (SPFx)

Über test-driven development (TDD) habe ich in einem vorherigen Beitrag geschrieben. Es sorgt für unaufgeregte Programmierarbeit und weniger Fehler. Aber wie viel Aufwand bedeutet es? Vor allem fragen sich oft SharePoint-Entwickler der Moderne (SharePoint Modern): „wie soll ich mit den vielen Fehlern fertig werden?“. So sieht ein typisches Bild aus, nachdem das Testframework jest zum ersten Mal zum Laufen gebracht wurde:

Fehlerlog eines Jest-Runs mit kryptischem Output

In diesem Artikel gehe ich davon aus, dass die Leserin oder Leser mit Unit Testing und mit den anderen Testtypen (End-to-End, Integration etc.) vertraut ist (wenn nicht freue ich mich über Feedback, dass ich hier mit einem Artikel nachbessern kann).

Marcin Wojciechowski schreibt in einem Microsoft 365 and Power Platform Community Blogpost über die Besonderheiten der Tests mit dem SharePoint-Framework (📄Englisch), dass Unit Testing – insbesondere in der SPFx-Welt – seiner Meinung nach schwierig sei. Und er ist nicht alleine. In meinen Projekten habe ich es häufig erlebt, wie ich selbst und Kollegen an obskuren Fehlermeldungen verzweifelten. Marcin gibt in seinem Artikel eine hilfreiche Anleitung, wie man Jest in ein SPFx-Projekt einbaut. Dies ist ein guter Startpunkt, aber Vorsicht – Wie der MVP Andrew Connell z.B. in einem empfehlenswerten ▶️ Video zum SPFx-Testing mit Jest verrät, gibt es zu Jest einige Besonderheiten, auf die ich unten eingehe.

Besonderheiten von Jest im SPFx-Kontext

  1. Microsoft hat sich aus irgendwelchen Gründen dazu entschieden, Mocha und Chai als Tech Stack für SPFx-Projekte einzubauen (nutzt aber angeblich intern Jest). Da Jest jedoch beide Funktionen von Mocha – testrunner – und Chai – assertion engine – ersetzen kann, ist es sogar möglich diese beiden aus dem Projekt zu deinstallieren (Rat von Andrew). Marcin verzichtet in seinem Post darauf und nutzt Chai mit Jest – was an sich geht, macht das Setup jedoch aus meiner Sicht komplexer als nötig.
  2. Andrew stellt ein durchdachtes Konzept inklusive Vorlagen auf GitHub für die zu installierende npm-Pakete bereit, inklusive Jest-Konfiguration. Aber: Er nimmt Enzyme automatisch mit dazu. Enzyme ist ein Framework von Airbnb und ist speziell für react-Komponententests konzipiert. Komponententests sind ein Hybrid zwischen Unit Tests und Integrationstests. Am Anfang (oder wenn reactjs nicht zum Einsatz kommt) empfehle ich jedoch, Enzyme wegzulassen! Die ist für klassische Unit Tests durchaus nicht nötig.
  3. Jest ist nicht im gulp-Buildsystem für SPFx integriert, was durchaus einen Vorteil sein kann. Gulp test führt nämlich bei jeder Ausführung alle Schritte des „gulp bundle“-Kommandos noch einmal durch.

Drei Erfolgsbausteine für schmerzfreie Tests:

  1. Vorbereitung – Wurde das Testframework sauber installiert und eingebaut?
  2. Auswahl der Testsprache – Je dynamischer desto besser
  3. Wenn Testen umständlich ist, ist das ein Zeichen für Refactoring

1. Die Vorbereitung

Hier ist das Rezept für ein Minimalsetup, das für mich bisher gut funktioniert hat (getestet mit SPFx Webparts und App Customizers – NodeJS v.16 inklusive PnP-Framework und Office UI Fabric bzw. Fluent UI components wie es umbenannt wurde). Die Einstellungen basieren teilweise auf das Beispielrepo spfx-unit-testing-pnpjs von dem Nutzer nbelyh (kudos!). Ich habe wie oben aufgeführt auf Chai verzichtet und babel-jest sowie dazugehörigen Plugins hinzugenommen wie im Repo von nbely. Diese sind notwendig, um den Output (in JavaScript-Format) sowie etwaige JS-Module für Jest aufbereiten zu können.

npm i --save-dev @types/jest identity-obj-proxy jest ts-jest @types/es6-promise babel-jest babel-plugin-transform-amd-to-commonjs babel-plugin-transform-es2015-modules-commonjs @babel/core @babel/preset-env

Um die Plugins in Babel integrieren zu können, muss eine neue babel.config.json Datei erzeugt werden (Ordner /src):

{
    "presets": [[ "@babel/preset-env", { "modules": false, "targets": { "node": "current" }}]],
    "plugins": ["transform-es2015-modules-commonjs", "transform-amd-to-commonjs"]
  }

Folgende Konfiguration für Jest ist noch notwendig. Ich habe es unten in package.json eingebaut, da es eine zusätzliche Konfigurationsdatei erspart (je einfacher, desto besser), aber grundsätzlich kann dieses auch als jest.config.json (oder das .js-Äquivalent) integriert werden. Es gibt ein paar Abweichungen gegenübar das Beispielrepo: ich habe die Einstellungen zur Lokalisierung rausgelassen, diese können jedoch bei Bedarf unter moduleNameMapper hinzugefügt werden. Außerdem habe ich .js-Dateien zur Testdatei-Definition (testRegex) hinzugefügt neben .ts und .tsx-Dateien – Warum erkläre ich beim Baustein Nummer 2. Auch habe ich die jest.setup.js-Datei rausgelassen da ich weder die Icons in Office UI Fabric noch die @pnp/spfx-controls-react in diesem Beispiel verwendet habe.

//...weitere Inhalte aus package.json hier entfernt (Lesbarkeit)
"scripts": {
    "build": "gulp bundle",
    "clean": "gulp clean",
    "test": "jest"
},
"jest": {
    "transform": {
      "^.+\\.(ts|tsx)$": "ts-jest",
      "^.+\\.js$": "babel-jest"
    },
    "transformIgnorePatterns": [
      "node_modules/(?!(@pnp|@uifabric|office-ui-fabric-react|@microsoft/sp-core-library|@microsoft/sp-http|@microsoft/sp-diagnostics|@microsoft/decorators|@microsoft/sp-page-context|@microsoft/sp-dynamic-data)/)"
    ],
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(ts?|tsx?|js)$",
    "moduleDirectories": [
      "node_modules"
    ],
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "json"
    ],
    "moduleNameMapper": {
      "\\.(css|scss)$": "identity-obj-proxy"
    }
  }

In tsconfig.json noch folgende Änderungen hinzufügen:

"types": [
      "es6-promise",
      "webpack-env",
      "jest"
    ],

Am Ende können Tests via „npm run test“ ausgeführt werden.

2. Auswahl der Testsprache

Beim Testen lohnt es sich zu hinterfragen: Welche Sprache nutze ich dabei und was habe ich davon? Es gibt hier viele Meinungen, ich neige aber dazu die Meinung von Paul Graham zu teilen, der in seinem Buch „Hackers and Painters“ noch 2010 dazu riet, die Sprache anhand der Art der Programmieraufgabe auszuwählen. Er vertritt die Meinung, dass dynamischere Sprachen sich eher für explorativen oder „plumbing“-Aufgaben eignen. Unit Testing und Testing allgemein erfüllen aus meiner Sicht beide Anforderungen – zumindest wenn richtig ausgeführt. Zum Thema „richtig ausgeführt“ lohnt sich eine Anmerkung, die einen eigenen Absatz verdient:

Tests sind nicht dazu da, Code Coverage-Quoten zu erfüllen!

Es gibt hier durchaus andere Ansichten, aber Tests um den Tests willen halte ich für falsch und eine Verschwendung von Zeit und Entwicklertalent. Achja, was mich zu einer weiteren Anmerkung bringt:

Tests sind nicht nur für Tester! Entwickler sollen (zumindest) den eigenen Code mit Unit Tests und bei Bedarf mit hybriden Tests versehen.

Warum Entwickler? Zum einen weil sie am besten in der Lage sind, sogenannten whitebox-Tests durchzuführen (Logik ist bekannt), zum anderen weil Tests den Entwickler dazu zwingen, die Qualität der eigenen Arbeit kritisch zu hinterfragen. Das führt – so meine Überzeugung – zu bessere Entwicklungsarbeit.

Aber zurück zum Thema Sprache: Im nächsten Abschnitt zeige ich TypeScript-Code auf, eine typisierte Sprache die bei SPFx-Lösungen häufig zum Einsatz kommt. TypeScript überlässt dem Programmierer selbst die Wahl, ob typisiert oder eher dynamisch. Das ist an sich einen guten Ansatz, wenn die Tests an sich jedoch auch in TypeScript geschrieben werden, führt dies erfahrungsgemäß zu einem erhöhten Aufwand, wo wir uns vorwiegend mit (zum Testzeitpunkt irrelevanten) Typprobleme beschäftigen, die ts-jest für uns aufwirft. Daher hier die dritte und letzte Anmerkung:

Für eine einfachere und reibungslosere Testentwicklung, lohnt es sich eher auf eine dynamische Sprache umzusteigen – in diesem Fall JavaScript.

Allein wenn man ein komplexeres Objekt mockt (d.h. ein Dummyobjekt für Testzwecke erzeugt) und man weiß, dass hier beispielsweise nur eine Eigenschaft zum Einsatz kommt, dann kann in JavaScript einfach das Objekt mit nur dieser Eigenschaft erzeugt werden. Natürlich kann man das auch mit TypeScript erreichen, die kognitiven Kosten sind aber aus meiner Sicht höher. Man muss einfach viel mehr beachten wenn die Tests in TypeScript geschrieben sind und es gibt kaum Vorteile. Daher hier meine klare Empfehlung: JavaScript für Jest-Tests einsetzen!

Links und Leseempfehlungen

🚧Hinweis: Dieser Artikel ist noch Work-In-Progress – Da fehlen noch zwei Teile 🙂 Diese werden bald mit Leben befüllt.

Kommentar verfassen