TypeScript und Unit-Tests: so sichern wir unsere JavaScript-Applikationen
17. August 2018 von Lars in News
Striktere Programmierung und Tests sichern unsere Anwendungen gegen Angriffe und sparen viel Zeit in der Fehlerbehebung!
Unit-Tests
Was sind Unit-Tests?
Ein Modultest (auch von englisch unit test als Unit-Test oder als Komponententest bezeichnet) wird in der Softwareentwicklung angewendet, um die funktionalen Einzelteile (Units) von Computerprogrammen zu testen, d. h., sie auf korrekte Funktionalität zu prüfen.
Im Klartext: Anwendungen bestehen aus vielen einzelnen Funktionen. Jede dieser Funktionen erfüllt im Idealfall genau eine Aufgabe. Entweder manipuliert sie einen Wert, gibt eine Information zurück, oder führt auf Grundlage einer Entscheidung andere Funktionen aus.
Sobald man eine Funktion schreibt, legt man fest, was diese Funktion machen soll. Und ob diese Funktion das auch wirklich macht, oder Fehler ordentlich behandelt wenn etwas schief läuft, das testet man mit automatisierten Tests. So stellt man sicher, dass, während die Anwendung wächst und wächst, und Daten und Funktionen immer wieder geändert und erweitert werden, Funktionen die bereits abgeschlossen sind immer noch genau die Funktionalität liefern, die sie sollen.
Wozu brauchen wir Unit-Tests?
Unit-Tests sind bei der Programmierung von Web-Anwendungen nichts neues und auch kein Hexenwerk. Jedoch machen sie nicht unbedingt bei jedem Projekt Sinn, oder sind im Budget mit unter zu bekommen.
Bei kleineren, überschaubaren Projekten z.B., bei denen Zeit und Budget sowieso knapp sind, ist es schwierig die notwendige Zeit noch in Code zu stecken, den sowohl Kunde als auch Anwender am Ende gar nicht mitbekommen.
Bei großen, kontinuierlich wachsenden Projekten sind Tests dagegen eine große Absicherung. Man kann mit ihnen verhindern, dass eine neue Funktion eine andere alte Funktion in ihrer Wirksamkeit beeinflusst. Also dass z.B. bereits abgeschlossene Komponenten und Features ein anderes Verhalten an den Tag legen, als sie ursprünglich sollten, und das obwohl man diese Komponenten am Ende vielleicht gar nicht angefasst hat.
Sogenannte Side-Effects können aber immer wieder auftreten, wenn man sich nicht durch Tests absichert.
Natürlich bieten auch Tests keine hundertprozentige Garantie, dass die Anwendung damit komplett Bug-Frei ist, aber sie helfen Probleme zu erkennen, bevor eine neue Version der Anwendung veröffentlicht wird.
Wie hoch ist der Aufwand?
Der Aufwand für Komponenten-Tests ist tatsächlich nicht zu unterschätzen, auch wenn er immer von der eigenen Sorgfalt abhängt.
Ein kleines Beispiel: Wir haben eine Vue-Funktion, die in Abhängigkeit des aktuellen Zustands der Komponente eine andere Funktion mit false
oder true
ausführen soll:
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
methods: {
swipeAction(direction: string): boolean {
if (this.isEditable) {
if (direction === 'left' && this.isVisible === false) {
this.emitToggleCard(true)
return true
} else if (direction === 'right' && this.isVisible === true) {
this.emitToggleCard(false)
return true
}
}
return false
}
}
})
</script>
Um die Funktion vollkommen durch Tests abzusichern müssen wir nun folgende Sachen testen:
- Gibt die Funktion
false
zurück, wennthis.isEditable === false
ist? - Ruft die Funktion
this.emitToggleCard
mitfalse
auf, wenn der Parameterdirection === 'right'
undthis.isVisible === true
ist? - Ruft die Funktion
this.emitToggleCard
mittrue
auf, wenn der Parameterdirection === 'left'
undthis.isVisible === true
ist?
Und so sehen die Tests dazu aus:
/**
* SwipeAction
*/
describe('SwipeAction', () => {
const stub = jest.fn()
let cmp: any
beforeEach(() => {
cmp = createCmp({ isEditable: true, cardId: '1', isHeadline: false, isVisible: true })
cmp.setMethods({ emitToggleCard: stub })
})
it('swipeAction calls toggleCard() with `true`', () => {
cmp.setProps({ isVisible: false })
cmp.vm.swipeAction('left')
expect(stub).toBeCalledWith(true)
})
it('swipeAction calls toggleCard() with false', () => {
cmp.setProps({ isVisible: true })
cmp.vm.swipeAction('right')
expect(stub).toBeCalledWith(false)
})
it('swipeAction returns false when card is not editable', () => {
cmp.setProps({ isEditable: false })
expect(cmp.vm.swipeAction('left')).toBe(false)
})
})
Wir schreiben hier also 3 Funktionen um EINE kleine Funktion zu testen. Und es ginge natürlich noch ausführlicher.
TypeScript
Was ist TypeScript?
TypeScript ist eine durch Microsoft entwickelte Programmiersprache, die auf den Vorschlägen zum zukünftigen ECMAScript-6-Standard basiert. Sprachkonstrukte von TypeScript, wie Klassen, Vererbung, Module, anonyme Funktionen und Generics wurden auch in ECMAScript 6 übernommen.
TypeScript ist also eine Programmiersprache die in reguläres JavaScript kompiliert. Der o.g. ECMAScript-Standard ist lediglich die nächste Versionsbezeichnung des weltweiten JavaScript-Standards.
Microsoft als Entwickler hinter TypeScript steckt viel Aufwand und Energie in die kontinuierliche Weiterentwicklung der Sprache, mit viel Zusammenarbeit mit der weltweiten Community.
Erst kürzlich erschien Version 3.0; gefühlt erscheint jeden Monat ein neues Update mit tollen Features und Bug-Fixes.
Wozu TypeScript?
Der große Vorteil von TypeScript: es schränkt die Freiheiten von regulären JavaScript ein. Klingt erstmal negativ, macht aber in punkto Sicherheit auf jeden Fall Sinn!
In JavaScript findet keine Typisierung von Variablen oder Funktionen statt. D.h. man muss beim erstellen einer Variable nicht angeben ob sie einen String, ein Objekt, einen Bool-Wert oder etwas anderes enthalten soll.
Ebenso können Funktionen alle möglichen Arten von Parametern erwarten und im Zweifelsfall alle möglichen Arten von Werten zurückgeben.
Dies macht den Code nicht nur anfälliger für Fehler und Sicherheitslücken, sondern für andere Entwickler auch schwerer lesbar.
TypeScript erleichtert also die Zusammenarbeit im Team, weil sich Code anderen Teammitgliedern schneller erschließt und einfach logischer aufgebaut ist.
Wie schwer ist der Einstieg?
Startet man neu mit TypeScript (und hat vorher nur JavaScript/ES6 geschrieben) könnte man von den vielen Möglichkeiten und Anforderungen tatsächlich etwas erschlagen werden.
Alles will definiert werden. Wirklich alles. Ein älteres jQuery-Plugin einbinden? Fehler: jQuery-Type muss erstmal definiert werden.
Ein Objekt mit Werten zusammenbauen? Zuerst das Objekt bitte erstmal vollständig mit allen Werten und Typen definieren!
Kleines Beispiel: ein Card-Objekt. Das Objekt soll wie folgt aufgebaut werden:
const card = {
isHeadline: true,
isVisible: true,
isUserContent: false,
isNote: false,
hasNote: false,
id: 'f9dcdc54-56dd-4baa-8c6e-bdcab5b03456',
title: 'Pick-up A: I’m from Greenich',
content: '',
userInfo: {
author: 'Herr Mustermann',
created: '2018-08-01T09:06:30.410Z',
updated: '2018-08-02T09:06:30.410Z'
}
}
Um dieses Objekt so anlegen zu können, und es später in Funktionen weiterverwenden zu können, müssen dafür sogenannte Interfaces
definiert werden, welche wie folgt aussehen:
interface IUserinfo {
author: String
created: String | Date
updated: String | Date
}
interface ICard {
isHeadline: Boolean
isVisible: Boolean
isUserContent: Boolean
isNote: Boolean
hasNote: Boolean
id: String
title: String
content: String
userInfo: IUserinfo | undefined
}
Mit diesen Interfaces können wir nun sagen, dass die Variable card
den Type ICard
besitzt, und nicht einfach ein undefiniertes Objekt mit zufälligen Inhalten ist:
const card: ICard = {
..
}
Das nur als kleines Beispiel, was möglich ist. Der Vorteil: ähnlich wie mit den Unit-Tests bestimmt man selbst, wie strikt man arbeitet. Man kann auch alle Warnungen von TypeScript ignorieren, und es wird trotzdem funktionaler Code erzeugt.
Haben Sie Fragen zu einem Projekt oder eine Idee? Sprechen Sie uns gern an oder nutzen Sie unser Kontaktformular. Wir freuen uns auf Ihre Fragen.