JavaScript UI Tests mit Struktur

Keine Kommentare

Mit einem guten Freund streite ich mich immer wieder über das Thema Tests in Web-Frontends. In der Regel vertauschen sich unsere Positionen – je nachdem, wie frustriert derjenige ist, der aktuell Tests schreibt.
Wir sprechen heute meistens eher von Web-Anwendungen als von Web-Frontends. Der Code dazu ist entsprechend umfangreich, obwohl moderne Web-Frameworks wie Angular, React oder Vue dem Entwickler viel Tipparbeit abnehmen.
Um die Funktionalität der Anwendung nach Änderungen sicherzustellen, hilft eine gute Testabdeckung. Doch wenn nach jeder kleinen Änderung die Tests angepasst werden müssen, ist man schnell genervt von der Menge nötiger Änderungen an den Tests. Das muss nicht sein! In diesem Artikel möchte ich zeigen, dass man mit ein wenig Strategie und Struktur beim Schreiben von Tests eine Menge Frust vermeiden kann.

Setup

Zu Demonstrationszwecken erstellen wir eine einfache Vue-Anwendung. Keine Sorge, es werden keine Vue-Kenntnisse vorausgesetzt. Die vorgestellte Strategie ist universell für alle gängigen UI- und Test-Frameworks übertragbar. Das Projekt wird erzeugt mit

# npm i -g vue-cli
# vue init webpack .
 
? Generate project in current directory? Yes
? Project name better-tests
? Project description Tutorial: Bessere JS Tests schreiben
? Author Author <author@example.com>
? Vue build standalone
? Install vue-router? No
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm

Der Standard-Test

Mit diesen Einstellungen erzeugt uns vue-cli eine lauffähige Anwendung und einen Test für die derzeit einzige Komponente im Projekt. Der Test befindet sich in test/unit/specs/HelloWorld.spec.js und sieht so aus:

import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'
 
describe('HelloWorld.vue', () => {
  it('should render correct contents', () => {
    const Constructor = Vue.extend(HelloWorld)
    const vm = new Constructor().$mount()
    expect(vm.$el.querySelector('.hello h1').textContent)
      .toEqual('Welcome to Your Vue.js App')
  })
})

Für eine „Hello World“-Komponente ist das absolut in Ordnung. Unschön ist allerdings, dass im Test mit Selektoren gearbeitet wird. Falls aus Stilgründen die Überschrift in ein anderes Tag verschoben wird oder man die CSS-Klasse umbenennt, wird der Test fehlschlagen. In solchen Fällen muss man in der gesamten Test-Spezifikation alle verwaisten Selektoren korrigieren. In einer „Hello World“-Komponente mit genau einem Test ist das überschaubar. Selten findet man in produktivem Code derart einfache Komponente.

Die Idee

Ich empfehle eine Trennung zwischen Testlogik und der Logik für die Konstruktion und Beschreibung der aktuellen Ansicht. Konkret kann man das obige Beispiel wie folgt umschreiben:

import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'
 
class HelloWorldPage {
  constructor () {
    // zu testenden View erzeugen
    const Constructor = Vue.extend(HelloWorld)
    this.vm = new Constructor().$mount()
  }
 
  headline = () => this.vm.$el.querySelector('.hello h1').textContent; // Selektor für Überschrift
}
 
describe('HelloWorld.vue', () => {
  it('should render correct contents', () => {
    // enthält keine View spezifische Logik
    const view = new HelloWorldPage()
    expect(view.headline()).toEqual('Welcome to Your Vue.js App')
  })
})

Das Konstruieren des Views und die Selektoren sind aus dem Test verschwunden. Der Test enthält nur noch die tatsächliche Testlogik. Geht man noch einen Schritt weiter, verschiebt man die neue HelloWorldPage-Klasse in eine eigene Datei und importiert diese in der Test-Spezifikation. Auf diese Weise bereinigt man Tests vollständig von der Framework-Logik. Man könnte den Test für die gleiche Komponente verwenden, selbst wenn diese mit einem anderen Framework umgesetzt wird.

Fazit

Der wesentliche Vorteil dieses Vorgehens ist, dass Änderungen in der Präsentation, also dem HTML Markup, nur in der Zwischenschicht nachgezogen werden müssen. Die Testlogik bleibt unverändert, da sich durch Änderungen am Markup keine Logik ändert.

Johann Wagner

Johann spielt seit 2016 beim Team Stuttgart der codecentric AG. Er bezeichnet sich selbst als Señor Full Stack Developer und baut gerne Lösungen auf Basis von Java- und Cloud-Technologien. Am liebsten experimentiert er den ganzen Tag mit neuen JavaScript Frameworks und Technologien.

Kommentieren

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