Prueba de React Hooks con enzimas y biblioteca de pruebas de React | Programar Plus

A medida que empiece a utilizar los ganchos de React en sus aplicaciones, querrá asegurarse de que el código que escriba sea sólido. No hay nada como enviar un código defectuoso. Una forma de asegurarse de que su código esté libre de errores es escribir pruebas. Y probar los hooks de React no es muy diferente de cómo se prueban las aplicaciones de React en general.

En este tutorial, veremos cómo hacer eso haciendo uso de una aplicación de tareas pendientes construida con ganchos. Cubriremos la escritura de pruebas usando Ezyme y React Testing Library, las cuales pueden hacer precisamente eso. Si eres nuevo en Enzyme, de hecho lo publicamos hace un tiempo mostrando cómo se puede usar con Jest en las aplicaciones React. No es una mala idea verificar eso mientras profundizamos en las pruebas de los ganchos de React.

Esto es lo que queremos probar

Un componente de tareas bastante estándar se parece a esto:

import React, { useState, useRef } from "react";
const Todo = () => {
  const [todos, setTodos] = useState([
    { id: 1, item: "Fix bugs" },
    { id: 2, item: "Take out the trash" }
  ]);
  const todoRef = useRef();
  const removeTodo = id => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  const addTodo = data => {
    let id = todos.length + 1;
    setTodos([
      ...todos,
      {
        id,
        item: data
      }
    ]);
  };
  const handleNewTodo = e => {
    e.preventDefault();
    const item = todoRef.current;
    addTodo(item.value);
    item.value = "";
  };
  return (
    <div className="container">
      <div className="row">
        <div className="col-md-6">
          <h2>Add Todo</h2>
        </div>
      </div>
      <form>
        <div className="row">
          <div className="col-md-6">
            <input
              type="text"
              autoFocus
              ref={todoRef}
              placeholder="Enter a task"
              className="form-control"
              data-testid="input"
            />
          </div>
        </div>
        <div className="row">
          <div className="col-md-6">
            <button
              type="submit"
              onClick={handleNewTodo}
              className="btn btn-primary"
            >
              Add Task
            </button>
          </div>
        </div>
      </form>
      <div className="row todo-list">
        <div className="col-md-6">
          <h3>Lists</h3>
          {!todos.length ? (
            <div className="no-task">No task!</div>
          ) : (
            <ul data-testid="todos">
              {todos.map(todo => {
                return (
                  <li key={todo.id}>
                    <div>
                      <span>{todo.item}</span>
                      <button
                        className="btn btn-danger"
                        data-testid="delete-button"
                        onClick={() => removeTodo(todo.id)}
                      >
                        X
                      </button>
                    </div>
                  </li>
                );
              })}
            </ul>
          )}
        </div>
      </div>
    </div>
  );
};
export default Todo; 

Prueba con enzima

Necesitamos instalar los paquetes antes de que podamos comenzar a probar. ¡Es hora de encender la terminal!

npm install --save-dev enzyme enzyme-adapter-16 

Dentro de src directorio, cree un archivo llamado setupTests.js. Esto es lo que usaremos para configurar el adaptador de Enzyme.

import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({ adapter: new Adapter() }); 

¡Ahora podemos empezar a escribir nuestras pruebas! Queremos probar cuatro cosas:

  1. Que el componente rinde
  2. Que las tareas pendientes iniciales se muestren cuando se procesa
  3. Que podemos crear una nueva tarea y recuperar otras tres
  4. Que podemos eliminar una de las tareas pendientes iniciales y solo nos queda una

En tus src directorio, cree una carpeta llamada __tests__ y crea el archivo donde escribirás las pruebas de tu componente Todo en él. Llamemos a ese archivo Todo.test.js.

Una vez hecho esto, podemos importar los paquetes que necesitamos y crear un describe bloque donde completaremos nuestras pruebas.

import React from "react";
import { shallow, mount } from "enzyme";
import Todo from "../Todo";

describe("Todo", () => {
  // Tests will go here using `it` blocks
});

Prueba 1: el componente se renderiza

Para ello, haremos uso de renderizado superficial. El renderizado superficial nos permite comprobar si se llama al método de renderizado del componente; eso es lo que queremos confirmar aquí porque esa es la prueba que necesitamos de que el componente se renderiza.

it("renders", () => {
  shallow(<Todo />);
});

Prueba 2: se muestran las tareas pendientes iniciales

Aquí es donde haremos uso de la mount método, que nos permite ir más allá de lo que shallow Nos da. De esa manera, podemos verificar la longitud de los elementos pendientes.

it("displays initial to-dos", () => {
  const wrapper = mount(<Todo />);
  expect(wrapper.find("li")).toHaveLength(2);
});

Prueba 3: podemos crear una nueva tarea y recuperar otras tres

Pensemos en el proceso involucrado en la creación de una nueva tarea pendiente:

  1. El usuario ingresa un valor en el campo de entrada.
  2. El usuario hace clic en el botón enviar.
  3. Obtenemos un total de tres elementos pendientes, donde el tercero es el recién creado.
it("adds a new item", () => {
  const wrapper = mount(<Todo />);
  wrapper.find("input").instance().value = "Fix failing test";
  expect(wrapper.find("input").instance().value).toEqual("Fix failing test");
  wrapper.find('[type="submit"]').simulate("click");
  expect(wrapper.find("li")).toHaveLength(3);
  expect(
    wrapper
      .find("li div span")
      .last()
      .text()
  ).toEqual("Fix failing test");
});

Montamos el componente y luego hacemos uso de find() y instance() métodos para establecer el valor del campo de entrada. Afirmamos que el valor del campo de entrada se establece en “Corregir prueba fallida” antes de seguir adelante para simular un evento de clic, que debería agregar el nuevo elemento a la lista de tareas pendientes.

Finalmente afirmamos que tenemos tres elementos en la lista y que el tercer elemento es igual al que creamos.

Prueba 4: podemos eliminar una de las tareas pendientes iniciales y solo nos queda una

it("removes an item", () => {
  const wrapper = mount(<Todo />);
  wrapper
    .find("li button")
    .first()
    .simulate("click");
  expect(wrapper.find("li")).toHaveLength(1);
  expect(wrapper.find("li span").map(item => item.text())).toEqual([
    "Take out the trash"
  ]);
});

En este escenario, devolvemos la tarea pendiente con un evento de clic simulado en el primer elemento. Se espera que esto llame al removeTodo() método, que debería eliminar el elemento en el que se hizo clic. Luego, comprobamos la cantidad de elementos que tenemos y el valor del que se devuelve.

El código fuente de estas cuatro pruebas está aquí en GitHub para que lo revise.

Prueba con react-testing-library

Escribiremos tres pruebas para esto:

  1. Que la tarea inicial se traduce
  2. Que podemos agregar una nueva tarea
  3. Que podemos borrar una tarea

Comencemos instalando los paquetes que necesitamos:

npm install --save-dev @testing-library/jest-dom @testing-library/react

A continuación, podemos importar los paquetes y archivos:

import React from "react";
import { render, fireEvent } from "@testing-library/react";
import Todo from "../Todo";
import "@testing-library/jest-dom/extend-expect";

test("Todo", () => {
  // Tests go here
}

Prueba 1: las renderizaciones iniciales de tareas pendientes

Escribiremos nuestras pruebas en un test cuadra. La primera prueba se verá así:

it("displays initial to-dos", () => {
  const { getByTestId } = render(<Todo />);
  const todos = getByTestId("todos");
  expect(todos.children.length).toBe(2);
});

¿Que esta pasando aqui? Estamos haciendo uso de getTestId para devolver el nodo del elemento donde data-testid coincide con el que se pasó al método. Eso es <ul> elemento en este caso. Luego, comprobamos que tiene un total de dos hijos (cada hijo es un <li> elemento dentro de la lista desordenada). Esto pasará ya que la tarea pendiente inicial es igual a dos.

Prueba 2: podemos agregar una nueva tarea pendiente

También estamos haciendo uso de getTestById aquí para devolver el nodo que coincide con el argumento que estamos pasando.

it("adds a new to-do", () => {
  const { getByTestId, getByText } = render(<Todo />);
  const input = getByTestId("input");
  const todos = getByTestId("todos");
  input.value = "Fix failing tests";
  fireEvent.click(getByText("Add Task"));
  expect(todos.children.length).toBe(3);
});

Usamos getByTestId para devolver el campo de entrada y el ul elemento como lo hicimos antes. Para simular un evento de clic que agrega un nuevo elemento de tarea, estamos usando fireEvent.click() y pasando en el getByText() método, que devuelve el nodo cuyo texto coincide con el argumento que pasamos. A partir de ahí, podemos verificar la longitud de las tareas pendientes verificando la longitud de la matriz de niños.

Prueba 3: podemos eliminar una tarea pendiente

Esto se parecerá un poco a lo que hicimos un poco antes:

it("deletes a to-do", () => {
  const { getAllByTestId, getByTestId } = render(<Todo />);
  const todos = getByTestId("todos");
  const deleteButton = getAllByTestId("delete-button");
  const first = deleteButton[0];
  fireEvent.click(first);
  expect(todos.children.length).toBe(1);
});

Estamos haciendo uso de getAllByTestId para devolver los nodos del botón eliminar. Como solo queremos eliminar un elemento, activamos un evento de clic en el primer elemento de la colección, que debería eliminar la primera tarea pendiente. Esto debería hacer que la longitud de todos niños iguales a uno.

Estas pruebas también están disponibles en GitHub.

Linting

Hay dos reglas de pelusa a seguir cuando se trabaja con ganchos:

Regla 1: Llamar a los ganchos en el nivel superior

… A diferencia de los condicionales internos, los bucles o las funciones anidadas.

// Don't do this!
if (Math.random() > 0.5) {
  const [invalid, updateInvalid] = useState(false);
}

Esto va en contra de la primera regla. Según la documentación oficial, React depende del orden en el que se llamen a los ganchos para asociar estado y el correspondiente useState llamar. Este código rompe el orden ya que solo se llamará al gancho si las condiciones son verdaderas.

Esto también se aplica a useEffect y otros ganchos. Consulte la documentación para obtener más detalles.

Regla 2: Llamar a hooks desde componentes funcionales de React

Los ganchos están destinados a ser utilizados en componentes funcionales de React, no en el componente de clase de React o una función de JavaScript.

Básicamente, hemos cubierto lo que no se debe hacer cuando se trata de pelar. Podemos evitar estos errores con un paquete npm que hace cumplir específicamente estas reglas.

npm install eslint-plugin-react-hooks --save-dev

Esto es lo que agregamos al archivo de configuración del paquete para que haga lo suyo:

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

Si está utilizando la aplicación Create React, debe saber que el paquete admite el complemento lint listo para usar a partir de la v3.0.0.

¡Continúa y escribe un código de React sólido!

Los ganchos de reacción son igualmente propensos a errores que cualquier otra cosa en su aplicación y querrá asegurarse de usarlos bien. Como acabamos de ver, hay un par de formas en que podemos hacerlo. Si utiliza Enzyme o puede utilizar la biblioteca de pruebas de enzima o React para escribir pruebas, depende totalmente de usted. De cualquier manera, intente hacer uso de la pelusa sobre la marcha y, sin duda, se alegrará de haberlo hecho.