Problem
Größere Komponenten in komplexeren Anwendungen benötigen häufig viele Daten. In einem Test für diese Komponenten muss man dann diese Daten erstellen, für mehrere Tests auch in mehreren Ausprägungen.
Dafür gibt es mehrere Möglichkeiten in TypeScript, hier am Beispiel eines Users:
Man erstellt sich je ein kleines Testobjekt nur mit den benötigten Feldern und casted auf den Type:
const testUser = { name: 'test' } as User;
Man erstellt sich jeweils ein vollständiges Testobjekt:
const testUser: User = { name: 'test', <plus 30 unneeded fields> };
Oder man erstellt sich ein vollständiges Objekt und spreaded dies in die "Abwandlungen":
const testUser: User = { name: 'test', <plus 30 unneeded fields> };
const testUserWithEmptyName: User = { ...testUser, name: '' };
Alle drei erwähnten Vorgehensweisen haben Nachteile:
- Casten führt dazu, dass der Test weiterhin kompiliert, auch wenn der Typ sich inkompatibel ändert.
- Die Testdateien werden komplett unleserlich und noch größer als sie wahrscheinlich sind.
- Zusätzlich zu (2.) muss man hier zwischen zwei Objekten hin und her springen, um zu erkennen, wie die Variable testUserWithEmptyName genau aussieht.
Lösung
Eine weitere, sehr praktische Möglichkeit ist ein Pattern, das ich "Testobject Creator" genannt habe.
Man lagert die Erstellung des Testobjekts in eine Funktion in einer eigenen Datei aus. Damit kann man sie auch in verschiedenen Tests, die diesen Typ von Objekt benötigen, verwenden. Diese Funktion gibt ein vollständiges Objekt des gewünschten Typs zurück, belegt mit default Werten. Gleichzeitig erlaubt die Funktion noch, per Parameter einzelne Felder zu überschreiben.
Beispiel
Bei unserem User (der Einfachheit halber nur mit fünf Feldern) könnte das so aussehen:
export const createUser = (user: Partial<User> = {}): User => ({
address: '',
email: '',
isAdmin: false,
id: 1,
name: '',
...user,
});
Verwendet wird es dann wie folgt:
const defaultUser = createUser();
const testUser = createUser({ name: 'test' });
const testUserWithEmptyName = createUser({ name: '' })
Wie man sieht, haben alle Felder im Creator leere Werte. Benötigt eine Komponente immer einen korrekt gesetzten Wert, kann man diesen auch direkt im Creator angeben. Man übergibt aber immer ein vollständiges Testobjekt mit korrekter Typisierung.
Der Parameter user hat den Typ Partial<User> . Das heißt, er ist wie der Typ User, nur dass beliebige Felder weggelassen werden können. Indem man diesen Parameter am Ende des Objekts mittels ...user "rein-spreaded" (siehe Spread syntax), wird die Default Belegung mit den übergebenen Feldern überschrieben.
(Anmerkung: Auch wenn der "name" im Creator bereits leer gesetzt wird, ist es bei "testUserWithEmptyName" sinnvoll, trotzdem "{ name: '' }" mit zu übergeben, um deutlich zu machen, um was es in diesem Test geht)
Weiterführende Aspekte:
- Mit dieser Implementierung ist das "reinspreaden" nur oberflächlich, es überschreibt also nur auf der obersten Ebene.
- Um einfach valide Testeinträge im Creator zu erstellen, kann beispielsweise faker-js verwendet werden.