Pruebas de componentes de React para humanos

Las pruebas de componentes de React deben ser interesantes, sencillas y fáciles de construir y mantener para un humano.
Sin embargo, el estado actual del ecosistema de bibliotecas de pruebas no es suficiente para motivar a los desarrolladores a escribir pruebas de JavaScript consistentes para los componentes de React. Las pruebas de componentes de React (y el DOM en general) a menudo requieren algún tipo de contenedor de nivel superior en torno a ejecutores de pruebas populares como Jest o Mocha.
Aquí está el problema
Escribir pruebas de componentes con las herramientas disponibles hoy en día es aburrido, e incluso cuando llegas a escribirlas, requiere muchas molestias. Expresar la lógica de prueba siguiendo un estilo similar a jQuery (encadenamiento) es confuso. No concuerda con la forma en que normalmente se construyen los componentes de React.
El código de Enzyme a continuación es legible, pero un poco voluminoso porque usa demasiadas palabras para expresar algo que, en última instancia, es un simple marcado.
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([ liJavaScript/li, liReactJs/li, liNodeJs/li, liWebpack/li])).to.equal(true);expect(screen.find("button").text()).toEqual("Back");expect(screen.find("button").hasClass("small")).to.equal(true);
La representación DOM es solo esta:
div className="view technologies" h3Technologies:/h3 ul liJavaScript/li liReactJs/li liNodeJs/li liWebpack/li /ul button className="small"Back/button/div
¿Qué sucede si necesita probar componentes más pesados? Si bien la sintaxis sigue siendo soportable, no ayuda al cerebro a comprender la estructura y la lógica. Leer y escribir varios solicitudes como este seguramente te cansará; Ciertamente me cansa a mí. Esto se debe a que los componentes de React siguen ciertos principios para generar código HTML al final. Por otra parte, las pruebas que expresan los mismos principios no son sencillas. El simple uso del encadenamiento de JavaScript no ayudará a largo plazo.
Hay dos problemas principales con las pruebas en React:
- Cómo abordar incluso la escritura de pruebas específicamente para componentes
- Cómo evitar todo el ruido innecesario
Ampliémoslos aún más antes de pasar a los ejemplos reales.
Acercándose a las pruebas de componentes de React
Un componente de React simple puede verse así:
function Welcome(props) { return h1Hello, {props.name}/h1;}
Esta es una función que acepta un props
objeto y devuelve un nodo DOM usando la sintaxis JSX.
Dado que un componente puede representarse mediante una función, se trata de probar funciones. Necesitamos tener en cuenta los argumentos y cómo influirán en el resultado devuelto. Al aplicar esa lógica a los componentes de React , el enfoque de las pruebas debe estar en configurar accesorios y probar el DOM representado en la interfaz de usuario . Dado que las acciones del usuario como mouseover
, click
escribir, etc. también pueden provocar cambios en la interfaz de usuario, deberá encontrar una manera de activarlos también mediante programación.
Ocultar el ruido innecesario en las pruebas.
Las pruebas requieren un cierto nivel de legibilidad que se logra reducir la redacción y seguir un patrón determinado para describir cada escenario.
Las pruebas de componentes pasan por tres fases:
- Organizar: Los accesorios componentes están preparados.
- Actuar: el componente debe representar su DOM en la interfaz de usuario y registrar cualquier acción del usuario (evento) que se activará mediante programación.
- Afirmar: Las expectativas se establecen, verificando ciertos efectos secundarios sobre el marcado del componente.
Este patrón en las pruebas unitarias se conoce como Arrange-Act-Assert.
Aquí hay un ejemplo:
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");});
Para pruebas más simples, las fases pueden fusionarse:
it("should render with a custom text", () = { // Mixing up all three phases into a single expect() call expect( // 1️⃣ Preparation ButtonSend/Button, // 2️⃣ Render "when mounted", // 3️⃣ Validation "to have text", "Send" );});
Escribir pruebas de componentes hoy
Esos dos ejemplos anteriores parecen lógicos pero no son nada triviales. La mayoría de las herramientas de prueba no proporcionan ese nivel de abstracción, por lo que tenemos que manejarlo nosotros mismos. Quizás el siguiente código parezca más familiar.
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);});
Compare eso con la misma prueba, solo que con una capa adicional de abstracción:
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" );});
Se ve mejor. Menos código y flujo obvio. Esta no es una prueba de ficción, sino algo que puedes lograr con UnexpectedJS hoy.
La siguiente sección es una inmersión profunda en las pruebas de componentes de React sin profundizar demasiado en UnexpectedJS. Su documentación hace más que el trabajo. En cambio, nos centraremos en el uso, los ejemplos y las posibilidades .
Escribir pruebas de React con UnexpectedJS
UnexpectedJS es un conjunto de herramientas de aserción extensible compatible con todos los marcos de prueba. Se puede ampliar con complementos y algunos de esos complementos se utilizan en el proyecto de prueba a continuación. Probablemente lo mejor de esta biblioteca es la práctica sintaxis que proporciona para describir casos de prueba de componentes en React.
El ejemplo: un componente de tarjeta de perfil
El tema de las pruebas es un componente de la tarjeta de perfil.
Y aquí está el código completo del componente de ProfileCard.js
:
// ProfileCard.jsexport 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 ? ( h3Bio/h3 p{bio !== "" ? bio : "No bio provided yet"}/p div button onClick={handleBioVisibility}View Skills/button p className="joined"Joined: {creationDate}/p /div / ) : ( h3Technologies/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 );}
Trabajaremos con la versión de escritorio del componente. Puede leer más sobre la división de código basado en dispositivos en React, pero tenga en cuenta que probar componentes móviles sigue siendo bastante sencillo.
Configurando el proyecto de ejemplo
No todas las pruebas se tratan en este artículo, pero sin duda veremos las más interesantes. Si desea seguir, ver este componente en el navegador o verificar todas sus pruebas, continúe y clone el repositorio de GitHub.
## 1. Clone the project:git clone git@github.com: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
Así es como ProfileCard /
se estructuran el componente y las pruebas de UnexpectedJS una vez que el proyecto ha comenzado:
/src └── /components ├── /ProfileCard | ├── ProfileCard.js | ├── ProfileCard.scss | └── ProfileCard.test.js └── /test-utils └── unexpected-react.js
Pruebas de componentes
Echemos un vistazo a algunas de las pruebas de componentes. Estos están ubicados en src/components/ProfileCard/ProfileCard.test.js
. Observe cómo cada prueba está organizada por las tres fases que cubrimos anteriormente.
- Configurar los accesorios de los componentes necesarios para cada prueba.
beforeEach(() = { props = { data: { name: "Justin Case", posts: 45, creationDate: "01.01.2021", }, };});
Antes de cada prueba, se compone un props
objeto con los accesorios necesarios, que contiene la información mínima para que el componente se represente.ProfileCard /
props.data
- Renderizar con estado en línea.
Ahora comprobamos si el perfil muestra el icono de estado “en línea”.
Y el caso de prueba para eso:
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" );});
- Renderizar con texto biográfico.
ProfileCard /
acepta cualquier cadena arbitraria para su biografía.
Entonces, escribamos un caso de prueba para eso:
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" );});
- Renderice la vista “Tecnologías” con una lista vacía.
Al hacer clic en el enlace "Ver habilidades", se debería cambiar a una lista de tecnologías para este usuario. Si no se pasan datos, entonces la lista debería estar vacía.
Aquí está ese caso de prueba:
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" );});
- Representar una lista de tecnologías.
Si se pasa una lista de tecnologías, se mostrará en la interfaz de usuario al hacer clic en el enlace "Ver habilidades".
Sí, otro caso de prueba:
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" }, ] } );});
- Representar la ubicación de un usuario.
Esa información debería aparecer en el DOM solo si se proporcionó como accesorio.
El caso de prueba:
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" );});
- Llamar a una devolución de llamada al cambiar de vista.
Esta prueba no compara los nodos DOM, pero verifica si una función pasada ProfileCard /
se ejecuta con el argumento correcto al cambiar entre las vistas Bio y Technologies.
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] );});
- Renderiza con un conjunto predeterminado de accesorios.
Una nota sobre la comparación de DOM:
la mayor parte del tiempo desea mantenerse alejado de los detalles de DOM en las pruebas. Utilice ID de prueba en su lugar.
Si por alguna razón necesita hacer valer la estructura DOM, consulte el ejemplo siguiente.
Esta prueba verifica todo el DOM producido por el componente al pasar los campos name
, posts
y creationDate
.
Esto es lo que produce el resultado en la interfaz de usuario:
Y aquí está el caso de prueba:
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" h2Justin Case/h2 i className="photo" / span45{" posts"}/span i className="status offline" / /div div className="details bio" h3Bio/h3 pNo bio provided yet/p div buttonView Skills/button p className="joined"{"Joined: "}01.01.2021/p /div /div /div );});
Ejecutando todas las pruebas
Ahora, todas las pruebas ProfileCard /
se pueden ejecutar con un simple comando:
yarn test
Observe que las pruebas están agrupadas. Hay dos pruebas independientes y dos grupos de pruebas para cada una de las ProfileCard /
vistas: bio y tecnologías. La agrupación hace que los conjuntos de pruebas sean más fáciles de seguir y es una buena manera de organizar unidades de interfaz de usuario relacionadas lógicamente.
Algunas palabras finales
Nuevamente, este pretende ser un ejemplo bastante simple de cómo abordar las pruebas de componentes de React. La esencia es considerar los componentes como funciones simples que aceptan accesorios y devuelven un DOM. A partir de ese momento , la elección de una biblioteca de prueba debe basarse en la utilidad de las herramientas que proporciona para manejar la representación de componentes y las comparaciones DOM. Según mi experiencia, UnexpectedJS es muy bueno en eso.
¿Cuáles deberían ser tus próximos pasos? ¡Mira el proyecto GitHub y pruébalo si aún no lo has hecho! Consulte todas las pruebas ProfileCard.test.js
y tal vez intente escribir algunas propias. También puede ver src/test-utils/unexpected-react.js
cuál es una función auxiliar simple que exporta funciones de bibliotecas de prueba de terceros.
Y, por último, aquí hay algunos recursos adicionales que sugeriría consultar para profundizar aún más en las pruebas de componentes de React:
- UnexpectedJS: la página oficial y los documentos de UnexpectedJS. Consulte también la sección Complementos.
- Sala UnexpectedJS Gitter: perfecta para cuando necesita ayuda o tiene una pregunta específica para los encargados de mantenimiento.
- Descripción general de las pruebas: puede probar los componentes de React de manera similar a probar otro código JavaScript.
- Biblioteca de pruebas de React: la herramienta recomendada para escribir pruebas de componentes en React.
- ¿En qué se diferencian los componentes funcionales de las clases? Dan Abramov describe los dos modelos de programación para crear componentes de React.
Deja un comentario