End-to-End-Tests werden geschrieben, um den Ablauf einer Anwendung von Anfang bis Ende zu überprüfen. Anstatt die Tests selbst durchzuführen – Sie wissen schon, manuelles Klicken auf der gesamten Anwendung – können Sie einen Test schreiben, der während Sie die Anwendung erstellen, ausgeführt wird. Das nennen wir kontinuierliche Integration und es ist eine schöne Sache. Schreiben Sie etwas Code, speichern Sie ihn und lassen Sie die Werkzeuge die schmutzige Arbeit erledigen, um sicherzustellen, dass nichts kaputt geht.
Cypress ist nur ein End-to-End-Testing-Framework, das all diese Klickarbeit für uns erledigt, und das werden wir in diesem Beitrag untersuchen. Es ist eigentlich für jede moderne JavaScript-Bibliothek gedacht, aber wir werden es in den Beispielen mit React integrieren.
Lassen Sie uns eine App zum Testen einrichten
In diesem Tutorial schreiben wir Tests, um eine von mir erstellte Todo-Anwendung abzudecken. Sie können das Repository klonen, um uns zu folgen, während wir es in Cypress integrieren.
git clone [email protected]:kinsomicrote/cypress-react-tutorial.git
Navigieren Sie in die Anwendung und installieren Sie die Abhängigkeiten
cd cypress-react-tutorial
yarn install
Cypress ist kein Teil der Abhängigkeiten, aber Sie können es installieren, indem Sie dies ausführen
yarn add cypress --dev
Führen Sie nun diesen Befehl aus, um Cypress zu öffnen
node_modules/.bin/cypress open

Das Tippen dieses Befehls wiederholt in das Terminal kann anstrengend sein, aber Sie können dieses Skript zur package.json-Datei im Stammverzeichnis des Projekts hinzufügen
"cypress": "cypress open"
Jetzt müssen Sie nur noch npm run cypress einmal ausführen und Cypress steht jederzeit bereit. Um ein Gefühl dafür zu bekommen, wie die Anwendung, die wir testen werden, aussieht, können Sie die React-Anwendung starten, indem Sie yarn start ausführen.
Wir beginnen damit, einen Test zu schreiben, um zu bestätigen, dass Cypress funktioniert. Erstellen Sie im Ordner cypress/integration eine neue Datei namens init.spec.js. Der Test besagt, dass true gleich true ist. Wir brauchen ihn nur, um zu bestätigen, dass er funktioniert, um sicherzustellen, dass Cypress für die gesamte Anwendung läuft.
describe('Cypress', () => {
it('is working', () => {
expect(true).to.equal(true)
})
})
Sie sollten eine Liste von Tests geöffnet haben. Gehen Sie dorthin und wählen Sie init.spec.js aus.

Dies sollte dazu führen, dass der Test ausgeführt wird und ein Bildschirm angezeigt wird, auf dem der Test erfolgreich ist.

Während wir uns noch in init.spec.js befinden, fügen wir einen Test hinzu, um zu überprüfen, ob wir die App besuchen können, indem wir https://:3000 im Browser aufrufen. Dies stellt sicher, dass die App selbst läuft.
it('visits the app', () => {
cy.visit('https://:3000')
})
Wir rufen die Methode visit() auf und übergeben ihr die URL der App. Wir haben Zugriff auf ein globales Objekt namens cy, um die uns von Cypress zur Verfügung stehenden Methoden aufzurufen.

Um die URL nicht immer wieder schreiben zu müssen, können wir eine Basis-URL festlegen, die für alle von uns geschriebenen Tests verwendet werden kann. Öffnen Sie die Datei cypress.json im Stammverzeichnis der Anwendung und fügen Sie die URL dort ein
{
"baseUrl": "https://:3000"
}
Sie können den Testblock wie folgt ändern
it('visits the app', () => {
cy.visit('/')
})
…und der Test sollte weiterhin erfolgreich sein. 🤞
Testen von Formularsteuerelementen und Eingabefeldern
Der Test, den wir schreiben werden, deckt die Interaktion der Benutzer mit der Todo-Anwendung ab. Zum Beispiel wollen wir sicherstellen, dass das Eingabefeld beim Laden der App im Fokus ist, damit Benutzer sofort mit der Eingabe von Aufgaben beginnen können. Wir wollen auch sicherstellen, dass eine Standardaufgabe vorhanden ist, damit die Liste standardmäßig nicht leer ist. Wenn keine Aufgaben vorhanden sind, wollen wir einen Text anzeigen, der dem Benutzer dies mitteilt.
Um zu beginnen, erstellen Sie eine neue Datei im Integrationsordner namens form.spec.js. Der Name der Datei ist nicht so wichtig. Wir stellen "form" voran, weil wir letztendlich ein Formularfeld testen. Sie möchten es vielleicht anders nennen, je nachdem, wie Sie Tests organisieren möchten.
Wir fügen der Datei einen describe-Block hinzu
describe('Form', () => {
beforeEach(() => {
cy.visit('/')
})
it('it focuses the input', () => {
cy.focused().should('have.class', 'form-control')
})
})
Der beforeEach-Block wird verwendet, um unnötige Wiederholungen zu vermeiden. Für jeden Testblock müssen wir die Anwendung besuchen. Es wäre redundant, diese Zeile jedes Mal zu wiederholen. beforeEach stellt sicher, dass Cypress die Anwendung in jedem Fall besucht.
Testen wir, ob das DOM-Element, das beim ersten Laden der Anwendung im Fokus ist, die Klasse form-control hat. Wenn Sie die Quelldatei überprüfen, werden Sie sehen, dass das Eingabeelement die Klasse form-control hat und autoFocus als eines der Elementattribute gesetzt ist.
<input
type="text"
autoFocus
value={this.state.item}
onChange={this.handleInputChange}
placeholder="Enter a task"
className="form-control"
/>
Wenn Sie das speichern, gehen Sie zurück zum Testbildschirm und wählen Sie form.spec.js aus, um den Test auszuführen.

Als Nächstes testen wir, ob ein Benutzer erfolgreich einen Wert in das Eingabefeld eingeben kann.
it('accepts input', () => {
const input = "Learn about Cypress"
cy.get('.form-control')
.type(input)
.should('have.value', input)
})

Wir haben Text ("Learn about Cypress") in das Eingabefeld eingegeben. Dann verwenden wir cy.get, um das DOM-Element mit der Klasse form-control zu erhalten. Wir könnten auch etwas wie cy.get('input') tun und das gleiche Ergebnis erzielen. Nachdem wir das Element erhalten haben, wird cy.type() verwendet, um den Wert einzugeben, den wir dem input zugewiesen haben, dann überprüfen wir, ob das DOM-Element mit der Klasse form-control einen Wert hat, der dem Wert von input entspricht.
Mit anderen Worten

Unsere Anwendung sollte auch zwei Todos haben, die standardmäßig erstellt wurden, wenn die App läuft. Es ist wichtig, dass wir einen Test haben, der überprüft, ob sie tatsächlich aufgelistet sind.
Was wollen wir? In unserem Code verwenden wir das Listenelement (<li>), um Aufgaben als Elemente in einer Liste anzuzeigen. Da wir standardmäßig zwei Elemente aufgeführt haben, bedeutet dies, dass die Liste zu Beginn eine Länge von zwei haben sollte. Der Test sieht also ungefähr so aus
it('displays list of todo', () => {
cy.get('li')
.should('have.length', 2)
})

Oh! Und was wäre diese App, wenn ein Benutzer keine neue Aufgabe zur Liste hinzufügen könnte? Das sollten wir auch besser testen.
it('adds a new todo', () => {
const input = "Learn about cypress"
cy.get('.form-control')
.type(input)
.type('{enter}')
.get('li')
.should('have.length', 3)
})
Das sieht ähnlich aus wie das, was wir in den letzten beiden Tests geschrieben haben. Wir erhalten das Eingabefeld und simulieren das Tippen eines Werts hinein. Dann simulieren wir das Absenden einer Aufgabe, die den Zustand der Anwendung aktualisieren sollte, wodurch sich die Länge von 2 auf 3 erhöht. Wir können also wirklich auf dem aufbauen, was wir bereits haben!

Wenn wir den Wert von drei auf zwei ändern, schlägt der Test fehl – das ist zu erwarten, da die Liste standardmäßig zwei Aufgaben haben sollte und das Absenden einmal insgesamt drei ergeben sollte.
Sie fragen sich vielleicht, was passieren würde, wenn der Benutzer eine (oder beide) der Standardaufgaben löscht, bevor er versucht, eine neue Aufgabe zu senden. Nun, wir könnten auch dafür einen Test schreiben, aber wir gehen in diesem Beispiel nicht davon aus, da wir nur bestätigen wollen, dass Aufgaben gesendet werden können. Dies ist eine einfache Möglichkeit für uns, die grundlegende Sende-Funktionalität beim Entwickeln zu testen, und wir können fortgeschrittene/Randfälle später berücksichtigen.
Das letzte Feature, das wir testen müssen, ist das Löschen von Aufgaben. Zuerst wollen wir eines der Standardaufgaben löschen und dann sehen, ob nach dem Löschen noch eines übrig ist. Es ist dasselbe wie zuvor, aber wir erwarten, dass noch ein Element in der Liste verbleibt, anstatt der drei, die wir erwartet haben, als wir eine neue Aufgabe zur Liste hinzugefügt haben.
it('deletes a todo', () => {
cy.get('li')
.first()
.find('.btn-danger')
.click()
.get('li')
.should('have.length', 1)
})
Okay, was passiert, wenn wir beide Standardaufgaben in der Liste löschen und die Liste komplett leer ist? Nehmen wir an, wir möchten diesen Text anzeigen, wenn keine Elemente mehr in der Liste sind: "Alle Ihre Aufgaben sind erledigt. Gut gemacht!"
Das ist nicht allzu anders als das, was wir bisher gemacht haben. Sie können es zuerst ausprobieren und dann zurückkommen, um den Code dafür zu sehen.
it.only('deletes all todo', () => {
cy.get('li')
.first()
.find('.btn-danger')
.click()
.get('li')
.first()
.find('.btn-danger')
.click()
.get('.no-task')
.should('have.text', 'All of your tasks are complete. Nicely done!')
})
Beide Tests sehen ähnlich aus: Wir erhalten das Listenelement, zielen auf das erste und verwenden cy.find(), um nach dem DOM-Element mit dem Klassennamen btn-danger zu suchen (was, wieder einmal, ein völlig willkürlicher Klassenname für den Löschknopf in dieser Beispiel-App ist). Wir simulieren ein Klickereignis auf das Element, um das Aufgaben-Element zu löschen.

Testen von Netzwerkanfragen
Netzwerkanfragen sind eine große Sache, da sie oft die Datenquelle für eine Anwendung sind. Nehmen wir an, wir haben eine Komponente in unserer App, die eine Anfrage an den Server sendet, um Daten zu erhalten, die dem Benutzer angezeigt werden. Nehmen wir an, das Markup der Komponente sieht so aus
class App extends React.Component {
state = {
isLoading: true,
users: [],
error: null
};
fetchUsers() {
fetch(`https://jsonplaceholder.typicode.com/users`)
.then(response => response.json())
.then(data =>
this.setState({
users: data,
isLoading: false,
})
)
.catch(error => this.setState({ error, isLoading: false }));
}
componentDidMount() {
this.fetchUsers();
}
render() {
const { isLoading, users, error } = this.state;
return (
<React.Fragment>
<h1>Random User</h1>
{error ? <p>{error.message}</p> : null}
{!isLoading ? (
users.map(user => {
const { username, name, email } = user;
return (
<div key={username}>
<p>Name: {name}</p>
<p>Email Address: {email}</p>
<hr />
</div>
);
})
) : (
<h3>Loading...</h3>
)}
</React.Fragment>
);
}
}
Hier verwenden wir die JSON Placeholder API als Beispiel. Wir können einen Test wie diesen haben, um die Antwort zu testen, die wir vom Server erhalten
describe('Request', () => {
it('displays random users from API', () => {
cy.request('https://jsonplaceholder.typicode.com/users')
.should((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.length(10)
expect(response).to.have.property('headers')
expect(response).to.have.property('duration')
})
})
})

Der Vorteil des Testens des Servers (im Gegensatz zum Stubben) ist, dass wir sicher sind, dass die Antwort, die wir erhalten, dieselbe ist, die ein Benutzer erhalten wird. Um mehr über Netzwerkanfragen zu erfahren und wie Sie Netzwerkanfragen stubben können, sehen Sie sich diese Seite in der Cypress-Dokumentation an.
Ausführen von Tests von der Befehlszeile
Cypress-Tests können vom Terminal aus ausgeführt werden, ohne die bereitgestellte UI
./node_modules/.bin/cypress run
...oder
npx cypress run
Lassen Sie uns die von uns geschriebenen Formular-Tests ausführen
npx cypress run --record --spec "cypress/integration/form.spec.js"
Das Terminal sollte die Ergebnisse direkt ausgeben, mit einer Zusammenfassung dessen, was getestet wurde.

In der Dokumentation erfahren Sie mehr über die Verwendung von Cypress mit der Befehlszeile.
Das ist ein Abschluss!
Tests sind etwas, das entweder Leute begeistert oder verängstigt, je nachdem, wen Sie fragen. Hoffentlich wird das, was wir in diesem Beitrag behandelt haben, alle für die Implementierung von Tests in einer Anwendung begeistern und zeigen, wie relativ einfach das sein kann. Cypress ist ein ausgezeichnetes Werkzeug und eines, das ich in meiner eigenen Arbeit gerne verwende, aber es gibt auch andere. Unabhängig davon, welches Werkzeug Sie verwenden (und wie Sie zu Tests stehen), hoffe ich, dass Sie die Vorteile des Testens erkennen und eher geneigt sind, es auszuprobieren.
Keine Erwähnung von data-test-id für Ihre Selektoren? Sie werden Leute auf lange Sicht in eine Welt des Schmerzes führen.
Hatten Sie Probleme mit
fetch? Soweit ich weiß, wirdfetchvon Cypress immer noch nicht unterstützt und Sie müssen diesen Trick anwenden.cy.get()funktioniert in React Apps nicht immer, da es versucht, die DOM-Elemente zu finden, bevor React sie rendert.