Als Ingenieure wollen wir Dinge bauen , die funktionieren , aber mit jeder neuen Funktion, die wir erstellen, erhöhen wir unweigerlich die Größe und Komplexität unserer Apps.
Wenn das Produkt wächst, wird es immer zeitaufwendiger, alle von Ihren Änderungen betroffenen Funktionen manuell (z. B. mit Ihren Händen) zu testen.
Das Fehlen automatisierter Tests führt dazu, dass wir entweder zu viel Zeit aufwenden und unsere Versandgeschwindigkeit verlangsamen oder zu wenig ausgeben, um die Geschwindigkeit zu sparen, was zusammen mit den nächtlichen Anrufen von PagerDuty zu neuen Fehlern im Rückstand führt.
Im Gegenteil, Computer können so programmiert werden, dass sie immer wieder dasselbe tun . Also delegieren wir das Testen an Computer!
Die Idee der Testpyramide schlägt drei Haupttypen von Tests vor: Unit-, Integrations- und End-to-End-Tests . Lassen Sie uns tief in jede Art eintauchen und verstehen, warum wir jede brauchen.
Eine Einheit ist ein kleines Stück Logik, das Sie isoliert testen (ohne sich auf andere Komponenten zu verlassen).
Unit-Tests sind schnell. Sie sind innerhalb von Sekunden fertig. Durch die Isolation können sie sie jederzeit lokal und auf CI ausführen, ohne die abhängigen Dienste hochzufahren bzw. API- und Datenbankaufrufe durchzuführen.
Beispiel für einen Unit-Test: Eine Funktion, die zwei Zahlen akzeptiert und diese summiert. Wir wollen es mit unterschiedlichen Argumenten aufrufen und sicherstellen, dass der zurückgegebene Wert korrekt ist.
// Function "sum" is the unit const sum = (x, y) => x + y test('sums numbers', () => { // Call the function, record the result const result = sum(1, 2); // Assert the result expect(result).toBe(3) }) test('sums numbers', () => { // Call the function, record the result const result = sum(5, 10); // Assert the result expect(result).toBe(15) })
Ein interessanteres Beispiel ist die React-Komponente, die Text rendert, nachdem die API-Anfrage abgeschlossen ist. Wir müssen das API-Modul nachahmen, um die notwendigen Werte für unsere Tests zurückzugeben, die Komponente zu rendern und sicherzustellen, dass der gerenderte HTML-Code den von uns benötigten Inhalt enthält.
// "MyComponent" is the unit const MyComponent = () => { const { isLoading } = apiModule.useSomeApiCall(); return isLoading ? <div>Loading...</div> : <div>Hello world</div> } test('renders loading spinner when loading', () => { // Mocking the API module, so that it returns the value we need jest.mock(apiModule).mockReturnValue(() => ({ useSomeApiCall: jest.fn(() => ({ // Return "isLoading: false" for this test case isLoading: false })) })) // Execute the unit (render the component) const result = render(<MyComponent />) // Assert the result result.findByText('Loading...').toBeInTheDocument() }) test('renders text content when not loading', () => { // Mocking the API module jest.mock(apiModule).mockReturnValue(() => ({ useSomeApiCall: jest.fn(() => ({ // Return "isLoading: false" for this test case isLoading: false })) })) // Execute the unit (render the component) const result = render(<MyComponent />) // Assert the result result.findByText('Hello world').toBeInTheDocument() })
Wenn Ihre Einheit mit anderen Einheiten interagiert (Abhängigkeiten) , nennen wir das eine Integration . Diese Tests sind langsamer als Unit-Tests, testen aber, wie die Teile Ihrer App miteinander verbunden sind.
Beispiel für einen Integrationstest: Ein Dienst, der Benutzer in einer Datenbank erstellt. Dies erfordert, dass eine DB-Instanz ( Abhängigkeit ) verfügbar ist, wenn die Tests ausgeführt werden. Wir werden testen, ob der Dienst einen Benutzer aus der Datenbank erstellen und abrufen kann.
import db from 'db' // We will be testing "createUser" and "getUser" const createUser = name => db.createUser(name) // creates a user const getUser = name => db.getUserOrNull(name) // retrieves a user or null test("creates and retrieves users", () => { // Try to get a user that doesn't exist, assert Null is returned const nonExistingUser = getUser("i don't exist") expect(nonExistingUser).toBe(null); // Create a user const userName = "test-user" createUser(userName); // Get the user that was just created, assert it's not Null const user = getUser(userName); expect(user).to.not.be(null) })
Es handelt sich um einen End-to-End- Test, bei dem wir die vollständig bereitgestellte App testen, bei der alle ihre Abhängigkeiten verfügbar sind. Diese Tests simulieren am besten das tatsächliche Benutzerverhalten und ermöglichen es Ihnen, alle möglichen Probleme in Ihrer App zu erkennen, sie sind jedoch die langsamste Art von Tests.
Wann immer Sie End-to-End-Tests durchführen möchten, müssen Sie die gesamte Infrastruktur bereitstellen und sicherstellen, dass in Ihrer Umgebung Drittanbieter verfügbar sind.
Sie möchten sie nur für die geschäftskritischen Funktionen Ihrer App haben.
Schauen wir uns ein End-to-End-Testbeispiel an: Anmeldefluss. Wir möchten zur App gehen, die Anmeldedaten eingeben, sie absenden und die Willkommensnachricht sehen.
test('user can log in', () => { // Visit the login page page.goto('https://example.com/login'); // Fill in the login form page.fill('#username', 'john'); page.fill('#password', 'some-password'); // Click the login button page.click('#login-button'); // Assert the welcome message is visible page.assertTextVisible('Welcome, John!') })
Denken Sie daran, dass End-to-End-Tests langsamer sind als Integrationstests und Integrationstests langsamer als Unit- Tests.
Wenn die Funktion, an der Sie arbeiten, geschäftskritisch ist, sollten Sie erwägen, mindestens einen End-to-End- Test zu schreiben (z. B. zu überprüfen, wie die Anmeldefunktion bei der Entwicklung des Authentifizierungsablaufs funktioniert).
Neben geschäftskritischen Abläufen wollen wir so viele Randfälle und verschiedene Zustände der Funktion wie möglich testen. Mit Integrationstests können wir testen, wie die Teile der App zusammenarbeiten.
Es ist eine gute Idee, Integrationstests für Endpunkte und Clientkomponenten durchzuführen. Endpunkte sollten die Vorgänge ausführen, das erwartete Ergebnis liefern und keine unerwarteten Fehler auslösen.
Clientkomponenten sollten den richtigen Inhalt anzeigen und auf Benutzerinteraktionen so reagieren, wie Sie es erwarten.
Und schließlich: Wann sollten wir Unit-Tests wählen? Alle kleinen Funktionen, die isoliert getestet werden können, wie z. B. sum
, das die Zahlen summiert, Button
, der <button>
-Tag rendert, sind gute Kandidaten für Unit-Tests. Einheiten sind perfekt, wenn Sie den Test Driven Development- Ansatz verfolgen.
Schreiben Sie einige Tests! (Aber klein anfangen)
Führen Sie die oben genannten Schritte einmal aus, um zu verstehen, wie es funktioniert. Wiederholen Sie den Vorgang dann während einiger Funktions-/Fehlerarbeiten. Teilen Sie es dann mit Ihren Kollegen, damit Sie alle Tests schreiben, Zeit sparen und nachts besser schlafen können!