React-Komponententests sollten interessant, unkompliziert und für einen Menschen einfach zu erstellen und zu warten sein.
Doch der aktuelle Stand des Ökosystems von Testbibliotheken reicht nicht aus, um Entwickler zu motivieren, konsistente JavaScript-Tests für React-Komponenten zu schreiben. Das Testen von React-Komponenten – und dem DOM im Allgemeinen – erfordert oft eine Art von Wrapper auf höherer Ebene um beliebte Test-Runner wie Jest oder Mocha.
Hier ist das Problem
Das Schreiben von Komponententests mit den heute verfügbaren Werkzeugen ist langweilig, und selbst wenn man sie schreibt, ist es mit viel Aufwand verbunden. Testlogik im Stil von jQuery (Verkettung) auszudrücken, ist verwirrend. Es passt nicht dazu, wie React-Komponenten normalerweise aufgebaut sind.
Der untenstehende Enzyme-Code ist lesbar, aber etwas zu sperrig, da er zu viele Wörter verwendet, um etwas auszudrücken, das letztendlich einfache Markup ist.
expect(screen.find(".view").hasClass("technologies")).to.equal(true);
expect(screen.find("h3").text()).toEqual("Technologies:");
expect(screen.find("ul").children()).to.have.lengthOf(4);
expect(screen.contains([
<li>JavaScript</li>,
<li>ReactJs</li>,
<li>NodeJs</li>,
<li>Webpack</li>
])).to.equal(true);
expect(screen.find("button").text()).toEqual("Back");
expect(screen.find("button").hasClass("small")).to.equal(true);
Die DOM-Darstellung ist einfach diese
<div className="view technologies">
<h3>Technologies:</h3>
<ul>
<li>JavaScript</li>
<li>ReactJs</li>
<li>NodeJs</li>
<li>Webpack</li>
</ul>
<button className="small">Back</button>
</div>
Was, wenn Sie schwerere Komponenten testen müssen? Während die Syntax immer noch erträglich ist, hilft sie Ihrem Gehirn nicht, die Struktur und Logik zu erfassen. Das Lesen und Schreiben mehrerer Tests wie dieser wird Sie ermüden – mich ermüdet es sicherlich. Das liegt daran, dass React-Komponenten bestimmten Prinzipien folgen, um am Ende HTML-Code zu generieren. Tests, die dieselben Prinzipien ausdrücken, sind dagegen nicht einfach. Einfaches JavaScript-Chaining hilft auf lange Sicht nicht.
Es gibt zwei Hauptprobleme beim Testen in React
- Wie man das Schreiben von Tests speziell für Komponenten überhaupt angeht
- Wie man all den unnötigen Lärm vermeidet
Lassen Sie uns diese weiter ausführen, bevor wir zu den echten Beispielen übergehen.
React-Komponententests angehen
Eine einfache React-Komponente könnte so aussehen
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
Dies ist eine Funktion, die ein props-Objekt akzeptiert und einen DOM-Knoten mithilfe der JSX-Syntax zurückgibt.
Da eine Komponente durch eine Funktion dargestellt werden kann, geht es darum, Funktionen zu testen. Wir müssen Argumente und deren Einfluss auf das zurückgegebene Ergebnis berücksichtigen. Wendet man diese Logik auf React-Komponenten an, sollte sich der Fokus der Tests auf das Einrichten von Props und das Testen des im UI gerenderten DOMs richten. Da Benutzeraktionen wie mouseover, click, Tippen usw. ebenfalls zu UI-Änderungen führen können, müssen Sie einen Weg finden, diese auch programmatisch auszulösen.
Den unnötigen Lärm in Tests ausblenden
Tests erfordern ein gewisses Maß an Lesbarkeit, das sowohl durch eine schlankere Formulierung als auch durch ein bestimmtes Muster zur Beschreibung jedes Szenarios erreicht wird.
Komponententests durchlaufen drei Phasen
- Arrange (Vorbereiten): Die Komponenten-Props werden vorbereitet.
- Act (Ausführen): Die Komponente muss ihr DOM im UI rendern und alle Benutzeraktionen (Ereignisse) registrieren, die programmatisch ausgelöst werden sollen.
- Assert (Überprüfen): Die Erwartungen werden festgelegt und überprüfen bestimmte Nebeneffekte auf dem Komponenten-Markup.
Dieses Muster im Unit-Testing ist als Arrange-Act-Assert bekannt.
Hier ist ein Beispiel
it("should click a large button", () => {
// 1️⃣ Arrange
// Prepare component props
props.size = "large";
// 2️⃣ Act
// Render the Button's DOM and click on it
const component = mount(<Button {...props}>Send</Button>);
simulate(component, { type: "click" });
// 3️⃣ Assert
// Verify a .clicked class is added
expect(component, "to have class", "clicked");
});
Für einfachere Tests können die Phasen zusammengeführt werden
it("should render with a custom text", () => {
// Mixing up all three phases into a single expect() call
expect(
// 1️⃣ Preparation
<Button>Send</Button>,
// 2️⃣ Render
"when mounted",
// 3️⃣ Validation
"to have text",
"Send"
);
});
Komponententests heute schreiben
Die beiden obigen Beispiele *sehen* logisch aus, sind aber alles andere als trivial. Die meisten Testwerkzeuge bieten kein solches Abstraktionsniveau, daher müssen wir uns selbst darum kümmern. Vielleicht kommt Ihnen der folgende Code bekannter vor.
it("should display the technologies view", () => {
const container = document.createElement("div");
document.body.appendChild(container);
act(() => {
ReactDOM.render(<ProfileCard {...props} />, container);
});
const button = container.querySelector("button");
act(() => {
button.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
});
const details = container.querySelector(".details");
expect(details.classList.contains("technologies")).toBe(true);
});
Vergleichen Sie das mit demselben Test, nur mit einer zusätzlichen Abstraktionsschicht
it("should display the technologies view", () => {
const component = mount(<ProfileCard {...props} />);
simulate(component, {
type: "click",
target: "button",
});
expect(
component,
"queried for test id",
"details",
"to have class",
"technologies"
);
});
Es sieht besser aus. Weniger Code und ein offensichtlicher Ablauf. Das ist kein fiktiver Test, sondern etwas, das Sie heute mit UnexpectedJS erreichen können.
Der folgende Abschnitt ist eine Tiefenanalyse zum Testen von React-Komponenten, ohne *zu tief* in UnexpectedJS einzusteigen. Die Dokumentation erledigt das mehr als genug. Stattdessen werden wir uns auf Verwendung, Beispiele und Möglichkeiten konzentrieren.
React-Tests mit UnexpectedJS schreiben
UnexpectedJS ist ein erweiterbares Assertions-Toolkit, das mit allen Test-Frameworks kompatibel ist. Es kann mit Plugins erweitert werden, und einige dieser Plugins werden im untenstehenden Testprojekt verwendet. Das Beste an dieser Bibliothek ist wahrscheinlich die praktische Syntax, die sie für die Beschreibung von Komponententestfällen in React bietet.
Das Beispiel: Eine Profilkartenkomponente
Gegenstand der Tests ist eine Profilkartenkomponente.

Und hier ist der vollständige Komponentencode von ProfileCard.js
// ProfileCard.js
export default function ProfileCard({
data: {
name,
posts,
isOnline = false,
bio = "",
location = "",
technologies = [],
creationDate,
onViewChange,
},
}) {
const [isBioVisible, setIsBioVisible] = useState(true);
const handleBioVisibility = () => {
setIsBioVisible(!isBioVisible);
if (typeof onViewChange === "function") {
onViewChange(!isBioVisible);
}
};
return (
<div className="ProfileCard">
<div className="avatar">
<h2>{name}</h2>
<i className="photo" />
<span>{posts} posts</span>
<i className={`status ${isOnline ? "online" : "offline"}`} />
</div>
<div className={`details ${isBioVisible ? "bio" : "technologies"}`}>
{isBioVisible ? (
<>
<h3>Bio</h3>
<p>{bio !== "" ? bio : "No bio provided yet"}</p>
<div>
<button onClick={handleBioVisibility}>View Skills</button>
<p className="joined">Joined: {creationDate}</p>
</div>
</>
) : (
<>
<h3>Technologies</h3>
{technologies.length > 0 && (
<ul>
{technologies.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
)}
<div>
<button onClick={handleBioVisibility}>View Bio</button>
{!!location && <p className="location">Location: {location}</p>}
</div>
</>
)}
</div>
</div>
);
}
Wir werden mit der Desktop-Version der Komponente arbeiten. Sie können mehr über gerätedefinierte Code-Splits in React lesen, aber beachten Sie, dass das Testen von mobilen Komponenten immer noch ziemlich unkompliziert ist.
Einrichtung des Beispielprojekts
Nicht alle Tests werden in diesem Artikel behandelt, aber wir werden uns sicherlich die interessantesten ansehen. Wenn Sie mitverfolgen möchten, die Komponente im Browser ansehen oder alle ihre Tests überprüfen möchten, dann klonen Sie das GitHub-Repo.
## 1. Clone the project:
git clone [email protected]:moubi/profile-card.git
## 2. Navigate to the project folder:
cd profile-card
## 3. Install the dependencies:
yarn
## 4. Start and view the component in the browser:
yarn start
## 5. Run the tests:
yarn test
So sind die <ProfileCard />-Komponente und die UnexpectedJS-Tests strukturiert, sobald das Projekt gestartet ist
/src
└── /components
├── /ProfileCard
| ├── ProfileCard.js
| ├── ProfileCard.scss
| └── ProfileCard.test.js
└── /test-utils
└── unexpected-react.js
Komponententests
Werfen wir einen Blick auf einige der Komponententests. Diese befinden sich in src/components/ProfileCard/ProfileCard.test.js. Beachten Sie, wie jeder Test nach den drei zuvor behandelten Phasen organisiert ist.
- Vorbereitung der erforderlichen Komponenteneigenschaften für jeden Test.
beforeEach(() => {
props = {
data: {
name: "Justin Case",
posts: 45,
creationDate: "01.01.2021",
},
};
});
Vor jedem Test wird ein props-Objekt mit den erforderlichen <ProfileCard />-Props zusammengestellt, wobei props.data die minimalen Informationen für das Rendering der Komponente enthält.
- Rendering mit Online-Status.
Nun prüfen wir, ob das Profil mit dem "Online"-Status-Icon gerendert wird.

Und der Testfall dafür
it("should display online icon", () => {
// Set the isOnline prop
props.data.isOnline = true;
// The minimum to test for is the presence of the .online class
expect(
<ProfileCard {...props} />,
"when mounted",
"queried for test id",
"status",
"to have class",
"online"
);
});
- Rendering mit Bio-Text.
<ProfileCard /> akzeptiert beliebige Strings für seine Bio.

Schreiben wir also einen Testfall dafür
it("should display bio text", () => {
// Set the bio prop
props.data.bio = "This is a bio text";
// Testing if the bio string is rendered in the DOM
expect(
<ProfileCard {...props} />,
"when mounted",
"queried for test id",
"bio-text",
"to have text",
"This is a bio text"
);
});
- Rendering der "Technologies"-Ansicht mit einer leeren Liste.
Das Klicken auf den Link "View Skills" sollte zu einer Liste von Technologien für diesen Benutzer wechseln. Wenn keine Daten übergeben werden, sollte die Liste leer sein.

Hier ist dieser Testfall
it("should display the technologies view", () => {
// Mount <ProfileCard /> and obtain a ref
const component = mount(<ProfileCard {...props} />);
// Simulate a click on the button element ("View Skills" link)
simulate(component, {
type: "click",
target: "button",
});
// Check if the details element contains a .technologies className
expect(
component,
"queried for test id",
"details",
"to have class",
"technologies"
);
});
- Rendering einer Liste von Technologien.
Wenn eine Liste von Technologien übergeben wird, wird sie im UI angezeigt, wenn auf den Link "View Skills" geklickt wird.

Ja, ein weiterer Testfall
it("should display list of technologies", () => {
// Set the list of technologies
props.data.technologies = ["JavaScript", "React", "NodeJs"];
// Mount ProfileCard and obtain a ref
const component = mount(<ProfileCard {...props} />);
// Simulate a click on the button element ("View Skills" link)
simulate(component, {
type: "click",
target: "button",
});
// Check if the list of technologies is present and matches the prop values
expect(
component,
"queried for test id",
"technologies-list",
"to satisfy",
{
children: [
{ children: "JavaScript" },
{ children: "React" },
{ children: "NodeJs" },
]
}
);
});
- Rendering eines Benutzerstandorts.
Diese Information sollte nur dann im DOM gerendert werden, wenn sie als Prop bereitgestellt wurde.

Der Testfall
it("should display location", () => {
// Set the location
props.data.location = "Copenhagen, Denmark";
// Mount <ProfileCard /> and obtain a ref
const component = mount(<ProfileCard {...props} />);
// Simulate a click on the button element ("View Skills" link)
// Location render only as part of the Technologies view
simulate(component, {
type: "click",
target: "button",
});
// Check if the location string matches the prop value
expect(
component,
"queried for test id",
"location",
"to have text",
"Location: Copenhagen, Denmark"
);
});
- Aufrufen eines Callbacks beim Wechseln der Ansichten.
Dieser Test vergleicht keine DOM-Knoten, sondern prüft, ob eine an <ProfileCard /> übergebene Funktions-Prop mit dem richtigen Argument ausgeführt wird, wenn zwischen den Ansichten "Bio" und "Technologies" gewechselt wird.
it("should call onViewChange prop", () => {
// Create a function stub (dummy)
props.data.onViewChange = sinon.stub();
// Mount ProfileCard and obtain a ref
const component = mount(<ProfileCard {...props} />);
// Simulate a click on the button element ("View Skills" link)
simulate(component, {
type: "click",
target: "button",
});
// Check if the stub function prop is called with false value for isBioVisible
// isBioVisible is part of the component's local state
expect(
props.data.onViewChange,
"to have a call exhaustively satisfying",
[false]
);
});
- Rendering mit Standard-Props.
Ein Hinweis zum DOM-Vergleich
Meistens sollten Sie sich von den DOM-Details in den Tests fernhalten. Verwenden Sie stattdessen Test-IDs.
Wenn Sie aus irgendeinem Grund die DOM-Struktur überprüfen müssen, beachten Sie das folgende Beispiel.
Dieser Test überprüft das gesamte DOM, das von der Komponente beim Übergeben der Felder name, posts und creationDate erzeugt wird.
Hier ist, was das Ergebnis im UI erzeugt

Und hier ist der Testfall dafür
it("should render default", () => {
// "to exhaustively satisfy" ensures all classes/attributes are also matching
expect(
<ProfileCard {...props} />,
"when mounted",
"to exhaustively satisfy",
<div className="ProfileCard">
<div className="avatar">
<h2>Justin Case</h2>
<i className="photo" />
<span>45{" posts"}</span>
<i className="status offline" />
</div>
<div className="details bio">
<h3>Bio</h3>
<p>No bio provided yet</p>
<div>
<button>View Skills</button>
<p className="joined">{"Joined: "}01.01.2021</p>
</div>
</div>
</div>
);
});
Alle Tests ausführen
Nun können alle Tests für <ProfileCard /> mit einem einfachen Befehl ausgeführt werden
yarn test

Beachten Sie, dass die Tests gruppiert sind. Es gibt zwei unabhängige Tests und zwei Testgruppen für jede der Ansichten von <ProfileCard /> – Bio und Technologies. Gruppierung erleichtert die Verfolgung von Testsuiten und ist eine gute Möglichkeit, logisch zusammenhängende UI-Einheiten zu organisieren.
Einige abschließende Worte
Auch dies ist als recht einfaches Beispiel dafür gedacht, wie man React-Komponententests angeht. Die Essenz ist, Komponenten als einfache Funktionen zu betrachten, die Props akzeptieren und ein DOM zurückgeben. Von diesem Punkt an sollte die Wahl einer Testbibliothek auf der Nützlichkeit der Werkzeuge basieren, die sie für die Handhabung von Komponenten-Renderings und DOM-Vergleichen bietet. UnexpectedJS ist meiner Erfahrung nach sehr gut darin.
Was sind Ihre nächsten Schritte? Sehen Sie sich das GitHub-Projekt an und probieren Sie es aus, wenn Sie es noch nicht getan haben! Überprüfen Sie alle Tests in ProfileCard.test.js und versuchen Sie vielleicht, einige eigene zu schreiben. Sie können sich auch src/test-utils/unexpected-react.js ansehen, eine einfache Hilfsfunktion, die Features aus den Drittanbieter-Testbibliotheken exportiert.
Und schließlich hier einige zusätzliche Ressourcen, die ich empfehle, um noch tiefer in das Testen von React-Komponenten einzutauchen
- UnexpectedJS – Die offizielle Seite und Dokumentation für UnexpectedJS. Sehen Sie sich auch den Abschnitt Plugins an.
- UnexpectedJS Gitter-Raum – Perfekt, wenn Sie Hilfe benötigen oder eine spezifische Frage an die Maintainer haben.
- Testübersicht – Sie können React-Komponenten ähnlich wie andere JavaScript-Codes testen.
- React Testing Library – Das empfohlene Werkzeug zum Schreiben von Komponententests in React.
- Wie unterscheiden sich Funktionskomponenten von Klassen? – Dan Abramov beschreibt die beiden Programmiermodelle für die Erstellung von React-Komponenten.
Die Syntax wäre nett, aber die Aufforderung zum Vergleich von HTML wird zu Dingen führen, die testing-library zu verhindern versucht – Implementierungsdetails sind egal. Wenn ich <ul> gegen <ol> austausche, funktioniert die Komponente immer noch und der Test sollte bestehen, wenn die Daten dem Benutzer präsentiert werden.
Ansonsten könnten wir genauso gut Snapshot-Tests verwenden.
Sie sind jedoch nicht wirklich an das Testen der HTML-Ausgabe mit der Bibliothek gebunden. Das Festhalten am Ansatz von testing-library ist vollkommen in Ordnung.
Wenn Sie jedoch
austauschenmitkann der Test bestehen, aber die UI ist trotzdem kaputt. Einverstanden, das können wir mit Snapshots abdecken. Ich frage mich nur – lohnt sich der unnötige Aufwand…
Sehen Sie persönlich einen Wert in den folgenden Punkten? Oder wie erreichen Sie dasselbe auf bessere Weise?
Ich mag String-Literale als Testcode wirklich nicht.
Meine IDE hilft mir nicht beim Autovervollständigen!
+1 für die IDE-Notiz Dustin.
Die Vorteile wachsen mit der Wartung.
Stellen Sie sich vor, Sie überfliegen eine Datei mit vielen Assertions im Format von
expect(screen.getByRole('alert')).toHaveTextContent('Oops, failed to fetch!')oderexpect(screen.getByRole('button')).not.toHaveAttribute('disabled')… Albtraum :)Wenn Sie jedoch
<ul />mit<ol />austauschen.Ich entschuldige mich für die Verwirrung.
Im Fall des Bio-Textes wird derselbe Code angezeigt wie im Test für den Online-Status.
Scharfes Auge. Korrigiert.
Ich sehe den Wert nicht darin, solch eine schwere Assertion auf Markup aufzubauen. Es fühlt sich an, als würde ich die Komponente zweimal schreiben: einmal in der Komponente selbst und dann nochmal in den Tests.
Ich stimme zu, dass das Ändern von Tags die UI brechen kann, aber dafür scheinen Snapshot-Tests einfacher und weniger ausführlich zu sein.
Ich stimme jedoch zu, dass es ziemlich gut ist, keine Ereignisse in
actwrappen zu müssen – ziemlich so, wie wir es bei VueJS-Tests machen.Ich verstehe Ihren Punkt. Solche gemischten Markup-Tests sollten selten verwendet werden (ich mache es hauptsächlich für den Standardfall) und versuchen, Implementierungsdetails zu vermeiden, wo möglich.
Es scheint, dass Leute sich für
testing-libraryentscheiden, aber alles hat seine Vor- und Nachteile. Mal sehen, ob es ein stabiler Trend ist.Meine persönliche Vorliebe geht bisher dahin, Syntax wie die oben genannte zu bevorzugen, da sie im Vergleich zu der jQuery-ähnlichen Syntax in den meisten Bibliotheken natürlich wirkt.
Ich glaube, Sie haben versehentlich den Codebereich von
4. Render mit Bio-Textgleich dem von3. Rendering mit Online-Statusgemacht.Ich stimme den früheren Kommentaren zu, dass Tests gegen den erwarteten DOM-Baum zerbrechlich und nicht besonders hilfreich sind, um festzustellen, ob eine Komponente "funktioniert".
Was das Aussehen angeht, ist die einzige Lösung, die für mich funktioniert hat, die Entwicklung von Komponenten in Storybook. Visuelle Regressionen werden durch gut geschriebene Stories schnell entdeckt, und es gibt Werkzeuge für automatische visuelle Vergleiche, obwohl ich diesen Schritt noch nicht unternommen habe.
Für DOM-strukturagnostische Tests können Sie die folgende Syntax verwenden