
Wir haben ein Problem. Das Problem beim Testen. Das Problem beim Testen von React-Komponenten ist ziemlich grundlegend. Es geht um den Unterschied zwischen unit testing
und integration testing
. Es geht um den Unterschied zwischen dem, was wir Unit-Tests nennen, und dem, was wir Integrationstests nennen, der Größe und dem Umfang.
Es geht nicht darum, sich selbst zu testen, sondern um die Komponentenarchitektur. Informationen zum Unterschied zwischen Testkomponenten, eigenständigen Bibliotheken und endgültigen Anwendungen .
Jeder weiß, wie man einfache Komponenten testet (sie sind einfach), wahrscheinlich weiß man, wie man Anwendungen (E2E) testet. Wie man endliche und unendliche Dinge testet ...
Definieren Sie das Problem
Es gibt zwei verschiedene Möglichkeiten, um React Component zu testen - shallow
und alles andere, einschließlich mount
, React react-testing-library
, webdriver
und so weiter. Nur shallow
ist etwas Besonderes - der Rest verhält sich genauso.
Und dieser Unterschied betrifft die Größe und den Umfang - darüber, WAS getestet werden würde und nur teilweise wie .
Kurz gesagt - shallow
zeichnet nur Aufrufe von React.createElement auf, führt jedoch keine Nebenwirkungen aus, einschließlich des Renderns von DOM-Elementen. Dies ist ein (algebraischer) Nebeneffekt von React.createElement.
Jeder andere Befehl führt den Code aus, den Sie mit jedem Nebeneffekt angegeben haben, der ebenfalls ausgeführt wird. Wie es real wäre, und das ist das Ziel.
Und das Problem ist folgendes: you can NOT run each and every side effect
.
Warum nicht?
Funktionsreinheit? Reinheit und Unveränderlichkeit - die heiligen Kühe von heute. Und du schlachtest einen von ihnen. Die Axiome des Unit-Tests - keine Nebenwirkungen, Isolation, Verspottung, alles unter Kontrolle.
Aber das ist kein Problem für ... dumb components
. Sie sind dumm, enthalten nur die Präsentationsschicht, aber keine "Nebenwirkungen".
Aber das ist ein Problem für Containers
. Solange sie nicht dumm sind, enthalten, was sie wollen, und vollständig über Nebenwirkungen. Sie sind das Problem!
Wenn wir die Regeln für "Die richtige Komponente" definieren, können wir sie wahrscheinlich leicht testen - sie werden uns leiten und uns helfen.
TRDL: Die endliche Komponente
Intelligente und dumme Komponenten
Laut Dan Abramov sind Artikelpräsentationskomponenten:
- Sind besorgt darüber, wie die Dinge aussehen.
- Kann sowohl Präsentations- als auch Containerkomponenten
**
enthalten und hat normalerweise einige eigene DOM-Markups und Stile. - Erlauben Sie häufig die Eindämmung über this.props.children.
- Haben Sie keine Abhängigkeiten vom Rest der App, wie z. B. Flux-Aktionen oder Stores.
- Geben Sie nicht an, wie die Daten geladen oder mutiert werden.
- Empfangen Sie Daten und Rückrufe ausschließlich über Requisiten.
- Selten haben sie einen eigenen Status (wenn sie dies tun, ist es eher der UI-Status als Daten).
- Werden als Funktionskomponenten geschrieben, es sei denn, sie benötigen Status-, Lebenszyklus-Hooks oder Leistungsoptimierungen.
- Beispiele: Seite, Seitenleiste, Story, UserInfo, Liste.
- ....
- Und Container sind nur Daten- / Requisitenanbieter für diese Komponenten.
Nach den Ursprüngen: In der idealen Anwendung ...
Container sind der Baum. Komponenten sind Baumblätter.
Finde die schwarze Katze im dunklen Raum
Die geheime Sauce hier, eine Änderung, die wir in dieser Definition ändern müssen, ist in „Kann sowohl Präsentations- als auch Containerkomponenten enthalten **
“ versteckt. Lassen Sie mich den Originalartikel zitieren:
In einer früheren Version dieses Artikels habe ich behauptet, dass Präsentationskomponenten nur andere Präsentationskomponenten enthalten sollten. Ich glaube nicht mehr, dass dies der Fall ist. Ob eine Komponente eine Präsentationskomponente oder ein Container ist, ist das Implementierungsdetail. Sie sollten in der Lage sein, eine Präsentationskomponente durch einen Container zu ersetzen, ohne eine der Aufrufstellen zu ändern. Daher können sowohl Präsentations- als auch Containerkomponenten andere Präsentations- oder Containerkomponenten enthalten.
Ok, aber was ist mit der Regel, die die Einheit der Präsentationskomponenten testbar macht - "Keine Abhängigkeiten vom Rest der App" ?
Leider machen Sie durch das Einfügen von Containern in die Präsentationskomponenten die zweiten unendlich und fügen dem Rest der App eine Abhängigkeit hinzu.
Wahrscheinlich warst du nicht dazu bestimmt. Ich habe also keine andere Wahl, als dumme Komponenten endlich zu machen:
PRÄSENTATIONSKOMPONENTEN DÜRFEN NUR ANDERE PRÄSENTATIONSKOMPONENTEN ENTHALTEN
Und die einzige Frage, die Sie stellen sollten (Blick auf Ihre aktuelle Codebasis): Wie? : tableflip:?!
Heutzutage werden Präsentationskomponenten und -container nicht nur verwickelt, sondern manchmal auch nicht als "reine" Entitäten extrahiert (Hallo GraphQL).
Lösung 1 - DI
Lösung 1 ist einfach - enthalten Sie keine verschachtelten Container in der dummen Komponente - enthalten Sie slots
. Akzeptiere einfach "Inhalt" (Kinder) als Requisiten, und das würde das Problem lösen:
- Sie können dumme Komponenten ohne "den Rest Ihrer App" testen.
- Sie können die Integration mit Rauch / Integration / e2e-Test testen, nicht mit Tests.
// Test me with mount, with "slots emty". const PageChrome = ({children, aside}) => ( <section> <aside>{aside}</aside> {children} </section> ); // test me with shallow, or real integration test const PageChromeContainer = () => ( <PageChrome aside={<ASideContainer />}> <Page /> </PageChrome> );
Von Dan selbst genehmigt:
{% twitter 1021850499618955272%}
DI (sowohl Dependecy Injection als auch Dependency Inversion) ist hier wahrscheinlich eine äußerst wiederverwendbare Technik, die Ihr Leben viel, viel einfacher machen kann.
Punkt hier - Dumme Komponenten sind dumm!
Lösung 2 - Grenzen
Dies ist eine recht deklarative Lösung und könnte Solution 1
- deklarieren Sie einfach alle Erweiterungspunkte . Wickeln Sie sie einfach mit ... Boundary
const Boundary = ({children}) => ( process.env.NODE_ENV === 'test' ? null : children // or `jest.mock` ); const PageChrome = () => ( <section> <aside><Boundary><ASideContainer /></Boundary></aside> <Boundary><Page /></Boundary> </section> );
Dann können Sie Boundary
(nur Null) deaktivieren, um den Komponentenumfang zu verringern und ihn endlich zu machen .
Punkt hier - Die Grenze befindet sich auf der Ebene der stummen Komponenten. Die dumme Komponente steuert, wie dumm sie ist.
Lösung 3 - Tier
Entspricht Lösung 2, jedoch mit einer intelligenteren Grenze, die in der Lage ist, Ebenen oder Ebenen oder was auch immer Sie sagen, zu verspotten:
const checkTier = tier => tier === currentTier; const withTier = tier => WrapperComponent => (props) => ( (process.env.NODE_ENV !== 'test' || checkTier(tier)) && <WrapperComponent{...props} /> ); const PageChrome = () => ( <section> <aside><ASideContainer /></aside> <Page /> </section> ); const ASideContainer = withTier('UI')(...) const Page = withTier('Page')(...) const PageChromeContainer = withTier('UI')(PageChrome);
Auch wenn dies dem Beispiel "Grenze" fast ähnlich ist - Die dumme Komponente ist "Dumm", und Container steuern die Sichtbarkeit anderer Container.
Lösung 4 - Getrennte Bedenken
Eine andere Lösung besteht darin, Bedenken zu trennen! Ich meine - du hast es bereits getan und wahrscheinlich ist es Zeit, es zu nutzen.
Indem Sie connect
Komponente mit Redux oder GQL verbinden, produzieren Sie bekannte Container. Ich meine - mit bekannten Namen - Container(WrapperComponent)
. Sie können sie mit ihren Namen verspotten
const PageChrome = () => ( <section> <aside><ASideContainer /></aside> <Page /> </section> ); // remove all components matching react-redux pattern reactRemock.mock(/Connect\(\w\)/) // all any other container reactRemock.mock(/Container/)
Dieser Ansatz ist etwas unhöflich - er löscht alles , was es schwieriger macht, Contaiers selbst zu testen, und Sie können ein etwas komplexeres Verspotten verwenden, um das "erste" beizubehalten:
import {createElement, remock} from 'react-remock'; // initially "open" const ContainerCondition = React.createContext(true); reactRemock.mock(/Connect\(\w\)/, (type, props, children) => ( <ContainerCondition.Consumer> { opened => ( opened ? ( // "close" and render real component <ContainerCondition.Provider value={false}> {createElement(type, props, ...children)} <ContainerCondition.Provider> ) // it's "closed" : null )} </ContainerCondition.Consumer> )
Punkt hier: Es gibt keine Logik innerhalb oder Präsentation, keinen Container - alle Logik ist außerhalb.
Bonuslösung - Separate Bedenken
Sie können die enge Kopplung mit defaultProps
und diese Requisiten in Tests aufheben ...
const PageChrome = ({Content = Page, Aside = ASideContainer}) => ( <section> <aside><Aside/></aside> <Content/> </section> );
Also?
Daher habe ich gerade einige Möglichkeiten veröffentlicht, um den Umfang einer Komponente zu verringern und sie viel testbarer zu machen. Der einfache Weg, um einen gear
aus dem gearbox
zu holen. Ein einfaches Muster, um Ihnen das Leben zu erleichtern.
E2E-Tests sind großartig, aber es ist schwierig, einige Bedingungen, die in einem tief verschachtelten Feature auftreten können, zu simulieren und darauf vorbereitet zu sein. Sie müssen Unit-Tests durchführen, um verschiedene Szenarien simulieren zu können. Sie müssen Integrationstests durchführen, um sicherzustellen, dass alles ordnungsgemäß verdrahtet ist.
Sie wissen, wie Dan in seinem anderen Artikel schrieb :
Wenn sich eine Schaltfläche beispielsweise in einem von 5 verschiedenen Zuständen befinden kann (normal, aktiv, Schweben, Gefahr, deaktiviert), muss der Code, der die Schaltfläche aktualisiert, für 5 × 4 = 20 mögliche Übergänge korrekt sein - oder einige davon verbieten. Wie zähmen wir die kombinatorische Explosion möglicher Zustände und machen die visuelle Ausgabe vorhersehbar?
Während die richtige Lösung hier Zustandsmaschinen sind, ist es die Grundvoraussetzung, ein einzelnes Atom oder Molekül auswählen und damit spielen zu können.
Die Hauptpunkte dieses Artikels
- Präsentationskomponenten sollten nur andere Präsentationskomponenten enthalten.
- Container sind der Baum. Komponenten sind Baumblätter.
- Sie müssen nicht immer NICHT Container in Präsentationscontainern enthalten , sondern sie dürfen nur in Tests enthalten sein.
Sie können tiefer in das Problem eintauchen, indem Sie den mittleren Artikel lesen, aber hier lassen Sie uns den ganzen Zucker überspringen.
PS: Dies ist eine Übersetzung des Ru-Habr- Artikels Habr-Version .