Ihre Arbeitsmittel

Automatisierte Tests sind im Grunde nur Code, der einen Fehler ausgibt oder verursacht, wenn etwas nicht stimmt. Die meisten Bibliotheken oder Test-Frameworks bieten eine Vielzahl von Primitiven, die das Schreiben von Tests vereinfachen.

Wie im vorherigen Abschnitt erwähnt, bieten diese Primitive fast immer eine Möglichkeit, unabhängige Tests (auch als Testfälle bezeichnet) zu definieren und Assertions bereitzustellen. Assertions sind eine Möglichkeit, die Überprüfung eines Ergebnisses mit dem Ausgeben eines Fehlers zu kombinieren, wenn etwas nicht in Ordnung ist. Sie können als grundlegende Primitive aller Testprimitive betrachtet werden.

Auf dieser Seite wird ein allgemeiner Ansatz für diese Primitive beschrieben. Das von Ihnen gewählte Framework hat wahrscheinlich etwas Ähnliches, aber dies ist keine genaue Referenz.

Beispiel:

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

In diesem Beispiel wird eine Gruppe von Tests (manchmal auch als Suite bezeichnet) als „mathematische Tests“ erstellt. Außerdem werden drei unabhängige Testläufe definiert, die jeweils einige Assertions ausführen. Diese Testläufe können in der Regel einzeln behandelt oder ausgeführt werden, z. B. durch ein Filter-Flag im Test-Runner.

Assertion-Hilfsprogramme als Primitive

Die meisten Test-Frameworks, einschließlich Vitest, enthalten eine Sammlung von Assertion-Hilfsprogrammen für ein assert-Objekt, mit denen Sie schnell Rückgabewerte oder andere Status mit bestimmten expectation vergleichen können. Diese Erwartung sind oft als „gut“ bekannte Werte. Im vorherigen Beispiel wissen wir, dass die 13. Fibonacci-Nummer 233 sein sollte. Daher können wir dies direkt mit assert.equal bestätigen.

Möglicherweise haben Sie auch die Erwartung, dass ein Wert eine bestimmte Form annimmt, größer als ein anderer Wert ist oder ein anderes Attribut hat. In diesem Kurs wird nicht das gesamte Spektrum der möglichen Assertion-Hilfsprogramme abgedeckt, aber Test-Frameworks bieten immer mindestens die folgenden grundlegenden Prüfungen:

  • Eine wahrheitsgemäße Prüfung, die oft als "OK"-Prüfung bezeichnet wird, prüft, ob eine Bedingung erfüllt ist. Dies entspricht der Art und Weise, wie Sie eine if schreiben, die prüft, ob etwas erfolgreich oder richtig ist. Sie wird in der Regel als assert(...) oder assert.ok(...) angegeben und enthält einen einzelnen Wert sowie einen optionalen Kommentar.

  • Eine Gleichheitsprüfung wie im mathematischen Testbeispiel, bei der davon auszugehen ist, dass der Rückgabewert oder der Zustand eines Objekts einem bekanntermaßen fehlerfreien Wert entspricht. Diese gelten für primitive Gleichheit (z. B. für Zahlen und Strings) oder referenzielle Gleichheit (sind beide dasselbe Objekt). Intern sind dies nur eine „wahre“ Prüfung mit einem ==- oder ===-Vergleich.

    • JavaScript unterscheidet zwischen lockerer (==) und strikter Übereinstimmung (===). Die meisten Testbibliotheken bieten Ihnen die Methoden assert.equal bzw. assert.strictEqual.
  • Detaillierte Gleichheitsprüfungen, bei denen Gleichheitsprüfungen erweitert werden, um den Inhalt von Objekten, Arrays und anderen komplexeren Datentypen zu überprüfen sowie die interne Logik für das Durchlaufen von Objekten zum Vergleich. Diese sind wichtig, weil JavaScript keine integrierte Möglichkeit hat, den Inhalt zweier Objekte oder Arrays zu vergleichen. [1,2,3] == [1,2,3] ist beispielsweise immer „false“. Test-Frameworks enthalten oft deepEqual- oder deepStrictEqual-Hilfsprogramme.

Assertion-Hilfsprogramme, die zwei Werte vergleichen (anstatt nur auf korrekte Prüfung zu prüfen), verwenden in der Regel zwei oder drei Argumente:

  • Der tatsächliche Wert, der aus dem getesteten Code generiert oder den zu validierenden Status beschreibt.
  • Der erwartete Wert, in der Regel hartcodiert (z. B. eine Literalzahl oder ein String).
  • Ein optionaler Kommentar, in dem beschrieben wird, was erwartet wurde oder was möglicherweise fehlgeschlagen ist. Dieser Kommentar wird eingefügt, wenn diese Zeile fehlschlägt.

Außerdem ist es recht üblich, Assertions zu kombinieren, um eine Reihe von Prüfungen zu erstellen, da es selten vorkommen kann, dass sich der Zustand Ihres Systems selbst richtig bestätigen lässt. Beispiel:

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

Vitest verwendet die Chai-Assertion-Bibliothek intern, um seine Assertion-Hilfsprogramme bereitzustellen. Es kann nützlich sein, sich die Referenz anzusehen, um zu sehen, welche Assertions und Hilfsprogramme für Ihren Code geeignet sind.

Fluent- und BDD-Assertions

Einige Entwickler bevorzugen einen Assertion-Stil, der als verhaltensorientierte Entwicklung (BDD) oder Fluent-Stil bezeichnet wird. Diese werden auch als „expect“-Hilfsprogramm bezeichnet, da der Einstiegspunkt zum Prüfen von Erwartungen eine Methode mit dem Namen expect() ist.

Sie verhalten sich genauso wie Assertions, die als einfache Methodenaufrufe wie assert.ok oder assert.strictDeepEquals geschrieben werden, aber für manche Entwickler sind sie einfacher zu lesen. Eine BDD-Assertion könnte so aussehen:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

Diese Art von Assertions funktioniert aufgrund einer Methode, die als Methodenverkettung bezeichnet wird. Dabei kann das von expect zurückgegebene Objekt kontinuierlich mit weiteren Methodenaufrufen verkettet werden. Einige Teile des Aufrufs, darunter to.be und that.does im vorherigen Beispiel, haben keine Funktion und sind nur enthalten, um das Lesen des Aufrufs zu erleichtern und eventuell einen automatischen Kommentar zu generieren, wenn der Test fehlschlägt. (Vor allem unterstützt expect normalerweise keinen optionalen Kommentar, da die Verkettung den Fehler klar beschreiben sollte.)

Viele Test-Frameworks unterstützen sowohl Fluent/BDD als auch reguläre Assertions. Vitest exportiert zum Beispiel beide Ansätze Chais und verfolgt einen etwas kürzeren Ansatz für das BDD. Jest hingegen enthält standardmäßig nur die Methode expect.

Tests dateiübergreifend gruppieren

Beim Schreiben von Tests neigen wir bereits dazu, implizite Gruppierungen bereitzustellen. Anstatt alle Tests in einer Datei zu speichern, ist es üblich, Tests für mehrere Dateien zu schreiben. Tatsächlich wissen Test-Runner in der Regel nur aufgrund eines vordefinierten Filters oder regulären Ausdrucks, dass eine Datei zum Testen vorgesehen ist. Vitest umfasst beispielsweise alle Dateien in Ihrem Projekt, die mit einer Erweiterung wie „.test.jsx“ oder „.spec.ts“ enden („.test“ und „.spec“ plus eine Reihe gültiger Erweiterungen).

Komponententests befinden sich in der Regel in einer Peer-Datei zur zu testenden Komponente, zum Beispiel in der folgenden Verzeichnisstruktur:

Eine Liste der Dateien in einem Verzeichnis, einschließlich Benutzerlisten.tsx und Kundenliste.test.tsx
Eine Komponentendatei und die zugehörige Testdatei

Ebenso werden Einheitentests tendenziell neben dem zu testenden Code platziert. End-to-End-Tests können jeweils in einer eigenen Datei gespeichert werden und Integrationstests können sogar in eigenen Ordnern abgelegt werden. Diese Strukturen können hilfreich sein, wenn komplexe Testläufe anwachsen und ihre eigenen Nicht-Test-Supportdateien erforderlich sind, z. B. Supportbibliotheken, die nur für einen Test benötigt werden.

Tests innerhalb von Dateien gruppieren

Wie in vorherigen Beispielen verwendet, ist es üblich, Tests in einen Aufruf von suite() zu platzieren, der Tests gruppiert, die Sie mit test() eingerichtet haben. Suiten sind normalerweise keine Tests, helfen aber beim Strukturieren, indem verwandte Tests oder Ziele durch Aufrufen der übergebenen Methode gruppiert werden. Bei test() beschreibt die übergebene Methode die Aktionen des Tests selbst.

Wie bei Assertions gibt es eine ziemlich standardmäßige Äquivalenz von Fluent/BDD zur Gruppierung von Tests. Im folgenden Code werden einige typische Beispiele verglichen:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

In den meisten Frameworks verhalten sich suite und describe ähnlich wie test und it, im Gegensatz zu den größeren Unterschieden zwischen der Verwendung von expect und assert zum Schreiben von Assertions.

Andere Tools verfolgen bei der Anordnung von Suiten und Tests sehr unterschiedliche Ansätze. Der integrierte Test-Runner von Node.js unterstützt beispielsweise verschachtelte Aufrufe von test(), um implizit eine Testhierarchie zu erstellen. Vitest lässt diese Art der Verschachtelung jedoch nur mit suite() zu und führt keine test() aus, die in einem anderen test() definiert ist.

Wie bei Assertions denken Sie daran, dass die genaue Kombination der Gruppierungsmethoden Ihres Technologie-Stacks nicht so wichtig ist. In diesem Kurs werden sie in der Zusammenfassung behandelt, aber Sie müssen herausfinden, wie sie auf die von Ihnen gewählte Tools anwendbar sind.

Lebenszyklusmethoden

Ein Grund für die Gruppierung von Tests, selbst implizit auf der obersten Ebene einer Datei, besteht darin, Einrichtungs- und Teardown-Methoden bereitzustellen, die für jeden Test oder einmal für eine Gruppe von Tests ausgeführt werden. Die meisten Frameworks bieten vier Methoden:

Für jede „test()“ oder „it()“ Einmal für die Suite
Vor den Testläufen `beforeEvery()` „beforeAll()“
Nach Testläufen „afterEvery()“ „afterAll()“

Sie können beispielsweise eine virtuelle Nutzerdatenbank vor jedem Test vorab mit Daten füllen und sie danach löschen:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

Dies kann zur Vereinfachung Ihrer Tests hilfreich sein. Sie können gemeinsamen Einrichtungs- und Bereinigungscode verwenden, anstatt ihn bei jedem Test zu duplizieren. Wenn der Einrichtungs- und Teardown-Code selbst einen Fehler ausgibt, kann dies auf strukturelle Probleme hinweisen, die nicht das Fehlschlagen der Tests selbst bedeuten.

Allgemeine Empfehlungen

Hier sind ein paar Tipps für diese Primitiven.

Primitives sind ein Leitfaden

Denken Sie daran, dass die Tools und Primitive hier und auf den nächsten Seiten nicht genau mit Vitest, Jest, Mocha, Web Test Runner oder einem anderen spezifischen Framework übereinstimmen. Vitest wurde zwar als allgemeiner Leitfaden verwendet, Sie sollten aber unbedingt das Framework Ihrer Wahl berücksichtigen.

Assertions nach Bedarf kombinieren

Tests sind im Wesentlichen Code, der Fehler ausgeben kann. Jeder Runner stellt eine einfache, wahrscheinlich test() bereit, um verschiedene Testfälle zu beschreiben.

Wenn dieser Runner jedoch auch assert()-, expect()- und Assertion-Hilfsprogramme bereitstellt, denken Sie daran, dass es in diesem Teil eher um Komfort geht und Sie ihn bei Bedarf überspringen können. Sie können jeden Code ausführen, der einen Fehler ausgeben könnte, einschließlich anderer Assertion-Bibliotheken, oder einer gut altmodischen if-Anweisung.

IDE-Einrichtung kann dir das Leben retten

Wenn Sie dafür sorgen, dass Ihre IDE, wie VSCode, Zugriff auf die automatische Vervollständigung und die Dokumentation für die von Ihnen ausgewählten Testtools hat, können Sie produktiver arbeiten. Beispielsweise gibt es über 100 Methoden für assert in der Chai-Assertion-Bibliothek. Es kann praktisch sein, die Dokumentation für die richtige inline anzeigen zu lassen.

Dies kann für einige Test-Frameworks, die den globalen Namespace mit ihren Testmethoden füllen, besonders wichtig sein. Dies ist ein subtiler Unterschied, aber oft ist es möglich, Testbibliotheken zu verwenden, ohne sie zu importieren, wenn sie dem globalen Namespace automatisch hinzugefügt werden:

// some.test.js
test('using test as a global', () => { … });

Wir empfehlen, die Hilfsprogramme zu importieren, auch wenn sie automatisch unterstützt werden, da Ihre IDE so eine übersichtliche Möglichkeit hat, diese Methoden nachzuschlagen. (Möglicherweise ist dieses Problem bereits beim Erstellen von React aufgetreten, da einige Codebasis ein magisches React-Element auf globaler Ebene hat, bei anderen aber nicht, und erfordern, dass es in allen Dateien mit React importiert wird.)

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });