Original article: How to Test React Components: the Complete Guide

Cuando comencé a aprender a probar mis aplicaciones en el pasado, me frustró mucho con los diferentes tipos, estilos y tecnologías utilizadas para las pruebas, junto con la variedad disuelta de publicaciones de blog, tutoriales y artículos. Descubrí que esto también es cierto para las pruebas de React.

Así que decidí escribir una guía completa de prueba de React en un artículo.

Guía completa, eh, ¿va a cubrir todos los escenarios de prueba posibles? Por supuesto que no. Sin embargo, será una guía básica completa para las pruebas y será suficiente para desarrollar la mayoría de los otros casos extremos.

También seleccioné una extensa colección de publicaciones de blog, artículos y tutoriales en la sección de lectura adicional al final que debería brindarle el conocimiento suficiente para estar en el 10% superior de los desarrolladores en términos de pruebas.

Puedes encontrar el proyecto terminado aquí:
https://github.com/iqbal125/react-hooks-testing-complete

tabla de contenido

teoría

  • ¿Qué son los Test?
  • ¿Por qué testear?
  • ¿Qué testear?
  • ¿Qué no testear?
  • como Testeo
  • Superficial vs Profundo
  • unidad vs integracion vs eae

informacion preliminar

  • algunas probabilidades y terminacion

enzima

  • Configuración de Enyme
  • renderizador de prueba de reaccion
  • prueba de instantaneas
  • probar los detalles de implementacion

Biblioteca de pruebas React

  • useState y props
  • usarReductor()
  • useContext()
  • Formularios de componentes controlados
  • Solicitudes de API de useEffect() y Axios

Ciprés

  • Una prueba completa de extremo a extremo

integracion continua

  • Travis.yml
  • Código Cobertura con overoles

teoria

¿Que es el Testin?

Comencemos por el principio y analicemos qué es la prueba. La prueba es un proceso de 3 pasos que se ve asi:

image-12

Arreglar, su aplicación está en un cierto estado original. Actúa, luego sucede algo (evento de clic, entrada, etc.). Luego afirma, o hace una hipótesis, del nuevo estado de su aplicación. Las pruebas pasarán si su hipótesis es correcta y fallarán si es incorrecta.

A diferencia de sus componentes de reacción, sus pruebas no se ejecutan en el navegador. Jest es el corredor de pruebas y el marco de prueba utilizado por React. Jest es el entorno donde se ejecuta realmente todas las pruebas. Es por eso que no necesita importar expecty describeen sus archivos. Estas funciones ya están disponibles globalmente en el entorno jest.

La sintaxis de sus pruebas se verá así:

describe('Testing sum', () => {
    function sum(a, b) {
       return a + b;
    }

    it('should equal 4',()=>{
       expect(sum(2,2)).toBe(4);
      })

    test('also should equal 4', () => {
        expect(sum(2,2)).toBe(4);
      }) 
});

describe envuelve nuestro bloque ito test, y es una forma de agrupar nuestras pruebas. Ambas ity testson palabras clave y se pueden usar indistintamente. La cadena será algo que debería suceder con sus pruebas y se imprimirá en la consola. toBe()es un comparador que funciona con esperar para permitirle hacer afirmaciones. Hay muchos más emparejadores y variables globales que ofrecemos broma, vea los enlaces a continuación para obtener una lista completa.

https://jestjs.io/docs/en/using-matchers

https://jestjs.io/docs/en/api

¿Qué testear?

Los test se realizan para garantizar que su aplicación funcione según lo previsto para sus usuarios finales. Tener pruebas hará que su aplicación sea más robusta y menos propensa a errores. Es una forma de verificar que el código está haciendo lo que pretendían los desarrolladores.

Posibles desventajas:

  • La redacción de la prueba requiere mucho tiempo y es difícil.
  • En ciertos escenarios, ejecutar test en CI puede costar dinero real.
  • Si se hace incorrectamente, puede darte falsos positivos. Sus pruebas pasan, pero su aplicación no funciona según lo previsto.
  • O falsos negativos. Sus pruebas fallan, pero su aplicación funciona según lo previsto.

¿Qué testear?

Para desarrollar el punto anterior, sus pruebas deben probar la funcionalidad de la aplicación, que imita cómo la usarán sus usuarios finales. Esto le dará la confianza de que su aplicación funcionará según lo previsto en su entorno de producción. Por supuesto, entraremos en muchos más detalles a lo largo de este artículo, pero esta es la esencia básica.

¿Qué no testear?

Me gusta usar la filosofía de Kent C dodds aquí de que no debe probar los detalles de implementación.

Detalles de implementación que significan probar cosas que no son la funcionalidad del usuario final. Veremos un ejemplo de esto en la sección Enzima a continuación.

Parece que está probando la funcionalidad allí, pero en realidad no es así. Estás probando el nombre de la función. Porque puede cambiar el nombre de la función y sus pruebas fallarán, pero su aplicación seguirá funcionando y le dará un falso negativo.

Tener que preocuparse constantemente por los nombres de funciones y variables es un dolor de cabeza, y tener que reescribir las pruebas cada vez que las cambia es tedioso, le mostraré un mejor enfoque.

Const variables: estas son variables que no cambian, no es necesario probarlas.

Bibliotecas de terceros: No es su trabajo probar estas bibliotecas. Depende de los creadores de estas bibliotecas probarlo. Si no está seguro de si una biblioteca está probada, no debe usarla. O puede leer el código fuente para ver si el autor incluye pruebas. Puede descargar el código fuente y ejecutar estas pruebas usted mismo. También puede preguntarle al autor si su biblioteca está lista para la producción o no.  

Mi filosofía personal sobre las pruebas

Gran parte de mi filosofía de evaluación se basa en las enseñanzas de Kent C dodds, por lo que verás muchos de sus sentimientos reflejados aquí, pero también algunos de mis propios pensamientos.

Muchas pruebas de integración. Sin pruebas instantáneas. Pocas pruebas unitarias. Pocas pruebas de ea e.

Las pruebas unitarias están un paso por encima de las pruebas instantáneas, pero no son ideales. Sin embargo, es mucho más fácil de entender y mantener que las pruebas instantáneas.

Escribir principalmente pruebas de integración. Las pruebas unitarias son buenas, pero en realidad no se parecen a la forma en que su usuario final interactúa con su aplicación. Es muy fácil probar los detalles de implementación con pruebas unitarias, especialmente con renderizado superficial.

Las pruebas de integración deben usarse lo menos posible

No se probaron detalles de implementación como nombres de funciones y variables.

Por ejemplo, si estamos probando un botón y cambiamos el nombre de la función en el método onClick de increment() a handleClick(), nuestras pruebas fallarían pero nuestro componente seguirá funcionando. Esta es una mala práctica porque básicamente solo estamos probando el nombre de la función, que es un detalle de implementación, que no le importa a nuestro usuario final.

Montaje superficial vs.

Mount en realidad ejecuta el código html, css y js como lo haría un navegador, pero lo hace de forma simulada. Es "sin cabeza", por ejemplo, lo que significa que no representa ni pinta nada en una interfaz de usuario, sino que actúa como un navegador web simulado y ejecuta el código en segundo plano.

No perder tiempo pintando nada en la interfaz de usuario hace que sus pruebas sean mucho más rápidas. Sin embargo, las pruebas de montaje siguen siendo mucho más lentas que las pruebas superficiales.

Esta es la razón por la que desmonta o limpia el componente después de cada prueba, porque es casi una aplicación en vivo y una prueba afectará a otra prueba.

Mount/render se usa normalmente para prueba de integración y superficial para prueba unitaria.

la renderización superficial solo renderiza el único componente que estamos probando. No renderiza componentes secundarios. Esto nos permite probar nuestro componente de forma aislada.

Por ejemplo, considere este componente hijo y padre.

import React from 'react';

const App = () => {
  return (
    <div> 
      <ChildComponent /> 
    </div> 
  )
}

const ChildComponent = () => {
  return (
    <div>
     <p> Child components</p>
    </div>
  )
}

Si usamos una representación superficial de App.jsobtendríamos algo como esto, observe que ninguno de los nodos DOM para el componente secundario está presente, de ahí el término representación superficial.

<App>
  <div> 
    <ChildComponent /> 
  </div>
</App> 

Ahora podemos comparar esto con el montaje del componente:

<App>
  <div> 
    <ChildComponent> 
      <div>
       <p> Child components</p>
      </div>
    </ChildComponent>
   </div>
</App> 

Lo que tenemos arriba está mucho más cerca de cómo se verá nuestra aplicación en el navegador, de ahí la superioridad de mount/render.

Unidad vs integración vs extremo a extremo

unit testing : probando una parte aislada de su aplicación, generalmente realizada en combinación con una representación superficial. Ejemplo: un componente se renderiza con los accesorios predeterminados.

pruebas de integración: probar si las diferentes partes funcionan o se integran entre sí. Por lo general, se realiza con el montaje o renderizado de un componente. Ejemplo: prueba si un componente secundario puede actualizar el estado del contexto en un componente principal.

e to e testing : significa "de extremo a extremo". Por lo general, una prueba de varios pasos que combina múltiples pruebas unitarias y de integración en una gran prueba. Por lo general, muy poco se burla o se critica. Las pruebas se realizan en un navegador simulado, puede haber o no una interfaz de usuario mientras se ejecuta la prueba. Ejemplo: probar un flujo de autenticación completo.

Informacion preliminar

react-testing-library: Personalmente, me gusta usar react-testing-library, pero la forma común es usar Enzyme. Le mostraré un ejemplo de Enzyme porque es importante conocer Enzyme en un nivel básico y el resto de los ejemplos con react-testing-library.

Esquema de ejemplos: Nuestros ejemplos seguirán un patrón. Primero le mostraré el componente React y luego las pruebas para él, con detalles detallados de cada uno. También puede seguir el repositorio vinculado al principio.

Configuración: También supondré que está utilizando create-react-app con la configuración de prueba predeterminada con jest, por lo que omitiré las configuraciones manuales.

Sinon, moca, chai: Gran parte de la funcionalidad que ofrece sinon está disponible de forma predeterminada con jest, por lo que no necesita sinon. Mocha y chai son un reemplazo para la broma. Jest viene preconfigurado de fábrica para funcionar con su aplicación, por lo que no tiene sentido usar Mocha y chai.

Esquema de nomenclatura de componentes: Mi esquema de nombres para los componentes es <TestSomething /> pero eso no significa que sean componentes falsos de ninguna manera. Son componentes regulares de React, este es solo el esquema de nombres.

npm test y jest watch mode :yarn test  trabajó para mi. npm testno funcionaba correctamente con el modo de reloj de broma.

probando un solo archivo : yarn test nombre del archivo

React Hooks vs Classes: Utilizo los componentes de React Hooks para la mayoría de los ejemplos, pero debido al poder de la biblioteca de pruebas de react, todas estas pruebas también funcionarán directamente con los componentes de la clase.

Con la información de fondo preliminar fuera del camino, podemos reparar algo de código.

enzima

Configuración de enzimas

Nuestras bibliotecas de terceros

npm install enzyme enzyme-to-json  enzyme-adapter-react-16

Comenzamos primero con nuestras importaciones.

import React from 'react';
import ReactDOM from 'react-dom';
import Basic from '../basic_test';

import Enzyme, { shallow, render, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() })

Comenzaremos con nuestras importaciones básicas. Nuestras primeras 3 importaciones son para reaccionar y nuestros componentes.

Después de esto importamos Enzyme. Luego importamos la función toJson de la biblioteca 'enzyme-to-json'. Necesitaremos esto para convertir nuestros componentes renderizados poco profundos en JSON, que se pueden guardar en el archivo de forma instantánea.

Finalmente, importamos nuestro Adaptador para hacer que la enzima funcione con React 16 y lo inicializamos como se muestra arriba.


react-test-renderer

React en realidad viene con su propio renderizador de prueba que puede usar en lugar de enzima y la sintaxis se verá así.

// import TestRenderer from 'react-test-renderer';
// import ShallowRenderer from 'react-test-renderer/shallow';


// Basic Test with React-test-renderer
// it('renders correctly react-test-renderer', () => {
//   const renderer = new ShallowRenderer();
//   renderer.render(<Basic />);
//   const result = renderer.getRenderOutput();
//
//   expect(result).toMatchSnapshot();
// });

Pero incluso los documentos de react-test-render sugieren usar enzima en su lugar porque tiene una sintaxis un poco más agradable y hace lo mismo. Sólo algo a tener en cuenta.

Prueba de instantaneas

Ahora nuestra primera prueba, que es una prueba instantánea.

it('renders correctly enzyme', () => {
  const wrapper = shallow(<Basic />)

  expect(toJson(wrapper)).toMatchSnapshot();
});

Si no ha ejecutado este comando antes, se creará automáticamente una carpeta _snapshots_ y un archivo test.js.snap. En cada prueba posterior, la nueva instantánea se comparará con el archivo de instantánea existente. La prueba pasará si la instantánea no ha cambiado y fallará si ha cambiado.

Básicamente, las pruebas instantáneas le permiten ver cómo ha cambiado su componente desde la última prueba, línea por línea. Las líneas de código que han cambiado se conocen como diff.

Aquí está nuestro componente básico que estamos probando instantáneas:

import React from 'react';


const Basic = () => {
  return (
    <div >
      <h1> Basic Test</h1>
         <p> This is a basic Test Component</p>
    </div>
  );
}

export default Basic;


Ejecutar la prueba anterior generará un archivo que se verá así. Este es esencialmente nuestro árbol de nodos React DOM.

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly enzyme 1`] = `
<div>
  <h1>
     Basic Test
  </h1>
  <p>
     This is a basic Test Component
  </p>
</div>
`;

Y producirá una estructura de carpetas que se verá así:

image-6


La salida de tu terminal se verá así:

image-7

Sin embargo, ¿qué sucede si cambiamos nuestro componente básico a este

import React from 'react';


const Basic = () => {
  return (
    <div >
      <h1> Basic Test</h1>

    </div>
  );
}

export default Basic;

Nuestras instantáneas ahora fallarán

image-8

Y también nos dará la diferencia.

image-9

Al igual que en git, el "-" antes de cada línea significa que se eliminó.

Solo necesitamos presionar "w" para activar el modo reloj y luego presionar "u" para actualizar la instantánea.

Nuestro archivo de instantáneas se actualizará automáticamente con la nueva instantánea y pasará nuestras pruebas

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly enzyme 1`] = `
<div>
  <h1>
     Basic Test
  </h1>
</div>
`;


Esto es solo para pruebas de instantáneas, pero si lees mi sección de pensamientos personales, sabes que no hago pruebas de instantáneas. Lo incluí aquí porque, al igual que Enzyme, es muy común y es algo que debe tener en cuenta, pero a continuación intentaré explicar por qué no lo uso.

Repasemos de nuevo qué es la prueba de instantáneas. Básicamente, le permite ver cómo ha cambiado su componente desde la última prueba. Cuáles son los beneficios de esto.

  • Es muy rápido y fácil de implementar y, a veces, solo requiere unas pocas líneas de código.
  • Puedes ver si nuestro componente se está renderizando correctamente. Puedes ver claramente los nodos DOM con la función .debug().

Contras, Argumentos en contra de las pruebas de instantáneas:

  • Lo único que hace una prueba instantánea es decirle si la sintaxis de su código ha cambiado desde la última prueba.
  • Entonces, ¿qué es lo que realmente está probando? Algunos dirían que no mucho.
  • También la representación básica de la aplicación correctamente es el trabajo de React, por lo que vas a probar un territorio de biblioteca de terceros.
  • También se pueden comparar diferencias con el control de versiones de git. Este no debería ser el trabajo de las pruebas de instantáneas.
  • Una prueba fallida no significa que su aplicación no esté funcionando según lo previsto, solo que su código ha cambiado desde la última vez que ejecutó la prueba. Esto puede generar muchos falsos negativos y una falta de confianza en la prueba. Esto también puede llevar a que las personas simplemente actualicen la prueba sin mirarla demasiado de cerca.
  • La prueba de instantáneas también le dice si su JSX es sintácticamente correcto, pero nuevamente, esto se puede hacer fácilmente en el entorno de desarrollo. Ejecutar una prueba de instantánea solo para verificar los errores de sintaxis no tiene ningún sentido.
  • Puede ser difícil entender lo que sucede en una prueba de instantáneas, ya que la mayoría de las personas usan pruebas de instantáneas con renderizado superficial, que no renderiza componentes secundarios, por lo que no le da al desarrollador ningún conocimiento.

Consulta la sección de lectura adicional para obtener más información.

Probando los detalles de implementación con Enzyme

Aquí daré un ejemplo de por qué no probar los detalles de implementación. Digamos que tenemos un componente de contador simple así:

import React, { Component } from 'react';


class Counter extends Component {
  constructor(props) {
    super(props)

    this.state = {
      count: 0
    }
  }

  increment = () => {
    this.setState({count: this.state.count + 1})
  }

  //This incorrect code will still cause tests to pass
  // <button onClick={this.incremen}>
  //   Clicked: {this.state.count}
  // </button>

  render() {
    return (
      <div>
        <button className="counter-button" onClick={this.incremen}>
          Clicked: {this.state.count}
        </button>
      </div>
  )}
}

export default Counter;

Notarás que tengo un comentario que sugiere que una aplicación que no funciona hará que las pruebas pasen, por ejemplo, al escribir mal el nombre de la función en el evento onClick.

Y veamos las pruebas que dejarán claro por qué.

import React from 'react';
import ReactDOM from 'react-dom';
import Counter from '../counter';

import Enzyme, { shallow, render, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() })

// incorrect function assignment in the onClick method
// will still pass the tests.

test('the increment method increments count', () => {
  const wrapper = mount(<Counter />)

  expect(wrapper.instance().state.count).toBe(0)

  // wrapper.find('button.counter-button').simulate('click')
  // wrapper.setState({count: 1})
  wrapper.instance().increment()
  expect(wrapper.instance().state.count).toBe(1)
})

Ejecutando el código anterior pasará las pruebas. También lo hará el uso wrapper.setState(). Así que tenemos pruebas de aprobación con una aplicación no funcional. No sé sobre usted, pero esto no me da confianza de que nuestra aplicación funcionará según lo previsto para nuestros usuarios finales.

Simular hacer clic en el botón no pasará las pruebas, pero podría darnos el problema opuesto, un falso negativo. Digamos que queremos cambiar el estilo del botón declarando una nueva clase CSS para él, una situación muy común. Nuestras pruebas ahora fallarán porque ya no podemos encontrar nuestro botón, pero nuestra aplicación seguirá funcionando, dándonos un falso negativo. Esto también es cierto cada vez que cambiamos los nombres de nuestras funciones o variables de estado.

Cada vez que queremos cambiar nuestra función y los nombres de clase CSS tenemos que reescribir nuestras pruebas, un proceso muy ineficiente y tedioso.

Entonces, ¿qué podemos hacer en su lugar?

React-testing-library

useState

De los documentos de la biblioteca de pruebas de react vemos que el principio rector principal es

Cuanto más se parezcan sus pruebas a la forma en que se usa su software, más confianza le pueden brindar.

Tendremos en cuenta este principio rector a medida que exploremos más con nuestras pruebas.

Comencemos con un componente básico de React Hooks y probemos el estado y las propiedades.

import React, { useState } from 'react';


const TestHook = (props) => {
  const [state, setState] = useState("Initial State")

  const changeState = () => {
    setState("Initial State Changed")
  }

  const changeNameToSteve = () => {
    props.changeName()
  }

  return (
  <div>
    <button onClick={changeState}>
      State Change Button
    </button>
    <p>{state}</p>
    <button onClick={changeNameToSteve}>
       Change Name
    </button>
    <p>{props.name}</p>
  </div>
  )
}


export default TestHook;

Nuestros accesorios provienen del componente principal root

  const App = () => {
      const [state, setState] = useState("Some Text")
      const [name, setName] = useState("Moe")
  ...
      const changeName = () => {
        setName("Steve")
      }

      return (
        <div className="App">
         <Basic />
        <h1> Counter </h1>
         <Counter />
        <h1> Basic Hook useState </h1>
         <TestHook name={name} changeName={changeName}/>
    ...     

Entonces, teniendo en cuenta nuestro principio rector, ¿cómo serán nuestras pruebas?

La forma en que nuestro usuario final usará esta aplicación será: ver un texto en la interfaz de usuario, ver el texto en el botón, luego hacer clic en él, finalmente ver un texto nuevo en la interfaz de usuario.

Así es como escribiremos nuestras pruebas usando la biblioteca de pruebas de React.

Utiliza este comando para instalar la biblioteca de pruebas de react.

npm install @testing-library/react

no

npm install react-testing-library

Ahora para nuestras pruebas

import React from 'react';
import ReactDOM from 'react-dom';
import TestHook from '../test_hook.js';
import {render, fireEvent, cleanup} from '@testing-library/react';
import App from '../../../App'

afterEach(cleanup)

it('Text in state is changed when button clicked', () => {
    const { getByText } = render(<TestHook />);

    expect(getByText(/Initial/i).textContent).toBe("Initial State")

    fireEvent.click(getByText("State Change Button"))

    expect(getByText(/Initial/i).textContent).toBe("Initial State Changed")
 })


it('button click changes props', () => {
  const { getByText } = render(<App>
                                <TestHook />
                               </App>)

  expect(getByText(/Moe/i).textContent).toBe("Moe")

  fireEvent.click(getByText("Change Name"))

  expect(getByText(/Steve/i).textContent).toBe("Steve")
})

Primero comenzamos con nuestras importaciones habituales.

A continuación tenemos la funcion afterEach(cleanup). Dado que no estamos usando renderizado superficial, tenemos que desmontar o limpiar después de cada prueba. Y esto es exactamente lo que está haciendo esta función.

getByText es el método de consulta que obtenemos al usar la desestructuración de objetos en el valor de la función de representación. Hay varios métodos de consulta más, pero este es el que querrá usar la mayor parte del tiempo.

Para probar nuestro aviso de estado, no estamos usando ningún nombre de función o los nombres de nuestras variables de estado. Mantenemos nuestro principio rector y no probamos los detalles de implementación. Dado que un usuario verá el texto en la interfaz de usuario, así es como consultaremos los nodos DOM. También consultaremos el botón de esta manera y haremos clic en él. Finalmente, también consultaremos el estado final en función del texto.

(/Initial/i) es una expresión regular que devuelve el primer nodo que al menos contiene el texto "Inicial".

es una expresión regular que devuelve el primer nodo que al menos contiene el texto "Inicial". App.js necesitaremos renderizarlo junto con nuestro componente. Al igual que en el ejemplo anterior, no estamos usando nombres de funciones y variables. Estamos probando la misma forma en que un usuario usaría nuestra aplicación y es a través del texto que verá.

Esperemos que esto le dé una buena idea de cómo probar con el react-testing-library y el principio rector, por lo general desea utilizar getByText la mayor parte del tiempo Hay algunas excepciones que veremos a medida que avancemos.

useReducer

Ahora podemos probar un componente con el gancho useReducer. Por supuesto, necesitaremos acciones y reductores para trabajar con nuestro componente, así que configurémoslos así:

Nuestra reductor

import * as ACTIONS from './actions'

export const initialState = {
    stateprop1: false,
}

export const Reducer1 = (state = initialState, action) => {
  switch(action.type) {
    case "SUCCESS":
      return {
        ...state,
        stateprop1: true,
      }
    case "FAILURE":
      return {
        ...state,
        stateprop1: false,
      }
    default:
      return state
  }
}

Y las acciones:




export const SUCCESS = {
  type: 'SUCCESS'
}

export const FAILURE = {
  type: 'FAILURE'
}

mantendremos las cosas simples y usaremos acciones en lugar de creadores de acciones.

Y finalmente el componente que utilizará estas acciones y reductores:

import React, { useReducer } from 'react';
import * as ACTIONS from '../store/actions'
import * as Reducer from '../store/reducer'


const TestHookReducer = () => {
  const [reducerState, dispatch] = useReducer(Reducer.Reducer1, Reducer.initialState)

  const dispatchActionSuccess = () => {
    dispatch(ACTIONS.SUCCESS)
  }

  const dispatchActionFailure = () => {
    dispatch(ACTIONS.FAILURE)
  }


  return (
    <div>
       <div>
        {reducerState.stateprop1
           ? <p>stateprop1 is true</p>
           : <p>stateprop1 is false</p>}
       </div>
       <button onClick={dispatchActionSuccess}>
         Dispatch Success
       </button>
    </div>
  )
}


export default TestHookReducer;

Este es un componente simple que cambiará stateprop1 De falsa a verdadera mediante el envío de una accio SUCCESS.

Y ahora nuestra prueba.

import React from 'react';
import ReactDOM from 'react-dom';
import TestHookReducer from '../test_hook_reducer.js';
import {render, fireEvent, cleanup} from '@testing-library/react';
import * as Reducer from '../../store/reducer';
import * as ACTIONS from '../../store/actions';


afterEach(cleanup)

describe('test the reducer and actions', () => {
  it('should return the initial state', () => {
    expect(Reducer.initialState).toEqual({ stateprop1: false })
  })

  it('should change stateprop1 from false to true', () => {
    expect(Reducer.Reducer1(Reducer.initialState, ACTIONS.SUCCESS ))
      .toEqual({ stateprop1: true  })
  })
})

it('Reducer changes stateprop1 from false to true', () => {
   const { container, getByText } = render(<TestHookReducer />);

   expect(getByText(/stateprop1 is/i).textContent).toBe("stateprop1 is false")

   fireEvent.click(getByText("Dispatch Success"))

   expect(getByText(/stateprop1 is/i).textContent).toBe("stateprop1 is true")
})

Primero comenzamos probando nuestro reductor. Y podemos envolver las pruebas para el reductor en el bloque describe. Estas son pruebas bastante básicas que estamos usando para asegurarnos de que initial state es lo que queremos y las acciones producen el resultado que queremos.

Puede argumentar que probar el reductor es probar los detalles de implementación, pero en la práctica descubrí que probar acciones y reductores es una prueba unitaria que siempre es necesaria.

Este es un ejemplo simple, por lo que no parece ser un gran problema, pero en aplicaciones más grandes y complejas, no probar los reductores y las acciones puede resultar desastroso. Por lo tanto, las acciones y los reductores serían una excepción a la regla de detalles de implementación de prueba.

A continuación tenemos nuestras pruebas para el componente real. Observe nuevamente que aquí no estamos probando los detalles de implementación. Usamos el mismo patrón del ejemplo anterior de useState. Obtenemos nuestros nodos DOM por el texto y también encontramos y hacemos clic en el botón con el texto.

useContext

Ahora avancemos y probemos si un componente secundario puede actualizar el estado del contexto en un componente principal. Esto puede parecer complejo, pero es bastante simple y directo.

Primero necesitaremos nuestro objeto de contexto que podemos inicializar en su propio archivo.

import React from 'react';

const Context = React.createContext()

export default Context

También necesitamos nuestro componente de aplicación principal que contendrá el proveedor de contexto. El valor transmitido al Provider será el valor del estado y de la función setState  del componente App.js.

import React, { useState } from 'react';
import TestHookContext from './components/react-testing-lib/test_hook_context';


import Context from './components/store/context';


const App = () => {
  const [state, setState] = useState("Some Text")
  

  const changeText = () => {
    setState("Some Other Text")
  }


  return (
    <div className="App">
    <h1> Basic Hook useContext</h1>
     <Context.Provider value={{changeTextProp: changeText,
                               stateProp: state
                                 }} >
        <TestHookContext />
     </Context.Provider>
    </div>
  );
}

export default App;

Y para nuestro componente

import React, { useContext } from 'react';

import Context from '../store/context';

const TestHookContext = () => {
  const context = useContext(Context)

  return (
    <div>
    <button onClick={context.changeTextProp}>
        Change Text
    </button>
      <p>{context.stateProp}</p>
    </div>
  )
}


export default TestHookContext;

Tenemos un componente simple que muestra el texto que inicializamos en App.js Y también pasamos la funcion setState del metodo onClick.

Nota: El estado es cambiado, inicializado y contenido en nuestro componente App.js.Simplemente hemos pasado el valor del estado y la funciób setState a nuestro componente secundario a través del contexto, pero en última instancia, el estado se maneja en el componente App.js. Esto será importante para entender nuestra prueba.

Y nuestra prueba:

import React from 'react';
import ReactDOM from 'react-dom';
import TestHookContext from '../test_hook_context.js';
import {act, render, fireEvent, cleanup} from '@testing-library/react';
import App from '../../../App'

import Context from '../../store/context';

afterEach(cleanup)

it('Context value is updated by child component', () => {

   const { container, getByText } = render(<App>
                                            <Context.Provider>
                                             <TestHookContext />
                                            </Context.Provider>
                                           </App>);

   expect(getByText(/Some/i).textContent).toBe("Some Text")

   fireEvent.click(getByText("Change Text"))

   expect(getByText(/Some/i).textContent).toBe("Some Other Text")
})

Incluso para el contexto, puedes ver que no rompemos nuestro patrón de pruebas, aún encontramos y simulamos nuestros eventos con el texto.

He incluido los componentes <Context.Provider/> y <TestHookContext /> en la función de representación porque hace que el código sea más fácil de leer, pero en realidad no necesitamos ninguno de ellos. Nuestra prueba seguirá funcionando si aprobamos solo el componente <App /> a la función de renderizado.

const { container, getByText } = render(<App/>) 

¿Por qué es este el caso?

Pensemos en lo que sabemos sobre el contexto. Todo el estado del contexto se maneja en App.js, por esta razón, este es el componente principal que estamos probando, aunque parece que estamos probando el componente secundario que usa el Hook useContext. Este código también funciona debido a mount/render. Como sabemos, en el renderizado superficial, los componentes secundarios son not rendered, pero son en mount/render. Ya que <Context.Provider /> y <TestHookContext /> ambos son componentes secundarios de <App /> se procesan automáticamente.

Formularios de componentes controlados

Un formulario de componente controlado esencialmente significa que el formulario funcionará a través del estado React en lugar de que el formulario mantenga su propio estado. Lo que significa que el controlador onChange guardará el texto de entrada en el estado React en cada pulsación de tecla.

Probar el formulario será un poco diferente de lo que hemos visto hasta ahora, pero intentaremos mantener nuestro principio rector en mente.

import React, { useState } from 'react';

const HooksForm1 = () => {
  const [valueChange, setValueChange] = useState('')
  const [valueSubmit, setValueSubmit] = useState('')

  const handleChange = (event) => (
    setValueChange(event.target.value)
  );

  const handleSubmit = (event) => {
    event.preventDefault();
    setValueSubmit(event.target.text1.value)
  };

    return (
      <div>
       <h1> React Hooks Form </h1>
        <form data-testid="form" onSubmit={handleSubmit}>
          <label htmlFor="text1">Input Text:</label>
          <input id="text1" onChange={handleChange} type="text" />
          <button type="submit">Submit</button>
        </form>
        <h3>React State:</h3>
          <p>Change: {valueChange}</p>
          <p>Submit Value: {valueSubmit}</p>
        <br />
      </div>
    )
}


export default HooksForm1;

Este es un formulario básico que tenemos aquí y también mostramos el valor del cambio y el valor de envío en nuestro JSX. Tenemos el atributo data-testid="form"  que usaremos en nuestra prueba para la consulta del formulario.

Y nuestras pruebas:

import React from 'react';
import ReactDOM from 'react-dom';
import HooksForm1 from '../test_hook_form.js';
import {render, fireEvent, cleanup} from '@testing-library/react';

afterEach(cleanup)

//testing a controlled component form.
it('Inputing text updates the state', () => {
    const { getByText, getByLabelText } = render(<HooksForm1 />);

    expect(getByText(/Change/i).textContent).toBe("Change: ")

    fireEvent.change(getByLabelText("Input Text:"), {target: {value: 'Text' } } )

    expect(getByText(/Change/i).textContent).not.toBe("Change: ")
 })


 it('submiting a form works correctly', () => {
     const { getByTestId, getByText } = render(<HooksForm1 />);

     expect(getByText(/Submit Value/i).textContent).toBe("Submit Value: ")

     fireEvent.submit(getByTestId("form"), {target: {text1: {value: 'Text' } } })

     expect(getByText(/Submit Value/i).textContent).not.toBe("Submit Value: ")
  })

Dado que un elemento de entrada vacío no tiene texto, usaremos una función getByLabelText() para obtener el nodo de entrada. Esto aún se mantendrá con nuestro principio rector, ya que el texto de la etiqueta es lo que el usuario leerá antes de ingresar el texto.

Ten en cuenta que activaremos el evento .change() en lugar del evento habitual .click() También pasamos datos ficticios en forma de:

{ target: { value: "Text" } }

Dado que se accederá al valor del formulario en forma de event.target.value,esto es lo que le pasamos al evento simulado.

Dado que generalmente no sabremos cuál es el texto que enviará el usuario, podemos usar una palabra clave .not para asegurarnos de que el texto haya cambiado en nuestro método de representación.

Podemos probar el envío del formulario de manera similar. La única diferencia es que usamos el evento .submit() y pasamos datos ficticios de esta manera:

{ target: { text1: { value: 'Text' } } }

Así es como se accede a los datos del formulario desde el evento sintético cuando un usuario envía un formulario. donde text1  es el id de nuestro elemento de entrada. Tendremos que romper nuestro patrón un poco aquí y usar el atributo data-testid="form" para consultar el formulario, ya que realmente no hay otra forma de obtener el formulario.

Y eso es todo por la forma. No es tan diferente de nuestros otros ejemplos. Si crees que lo entendiste, pasemos a algo un poco más complejo.

useEffect y solicitudes de API con axios

Ahora veamos cómo probaríamos el enlace useEffect hook y las solicitudes de API. sto será bastante diferente a lo que hemos visto hasta ahora.

Digamos que tenemos una URL pasada a un componente secundario desde el padre raíz.


...

     <TestAxios url='https://jsonplaceholder.typicode.com/posts/1' />
     
 ... 

Y el componente en sí.

import React, { useState, useEffect } from 'react';
import axios from 'axios';


const TestAxios = (props) => {
  const [state, setState] = useState()

  useEffect(() => {
    axios.get(props.url)
      .then(res => setState(res.data))
  }, [])


  return (
    <div>
    <h1> Axios Test </h1>
        {state
          ? <p data-testid="title">{state.title}</p>
          : <p>...Loading</p>}
    </div>
  )
}


export default TestAxios;

Simplemente hacemos una solicitud de API y guardamos los resultados en el estado local. También usamos una expresión ternaria en nuestro método de representación para esperar hasta que se complete la solicitud para mostrar los datos del título del marcador de posición json.

Notarás que nuevamente tendremos que hacer uso del atributo data-testid por necesidad, y nuevamente es un detalle de implementación ya que un usuario no verá ni interactuará con este atributo de ninguna manera, pero esto es más realista, ya que generalmente no conocerá el texto de una solicitud API de antemano.

También usaremos simulacros en esta prueba.

Un mock es una forma de simular un comportamiento que en realidad no queremos hacer en nuestras pruebas. Por ejemplo, nos burlamos de las solicitudes de API porque no queremos realizar solicitudes reales en nuestras pruebas.

No queremos realizar solicitudes de API reales en nuestras pruebas por varias razones: hará que nuestras pruebas sean mucho más lentas, podría darnos un falso negativo, la solicitud de API nos costará dinero o estropearemos nuestra base de datos con datos de prueba.

import React from 'react';
import ReactDOM from 'react-dom';
import TestAxios from '../test_axios.js';
import {act, render, fireEvent, cleanup, waitForElement} from '@testing-library/react';

import axiosMock from "axios";

Tenemos nuestras importaciones habituales pero notarás algo peculiar. Estamos importando axiosMock de la biblioteca axios. No estamos importando un objeto axios simulado de la biblioteca. En realidad, nos estamos burlando de la propia biblioteca de axios.

¿Cómo?

Mediante el uso de la funcionalidad de burla que ofrece jest.

Primero crearemos una __mocks__ adyacente a nuestra carpeta de prueba, algo como esto.

image-13

Y dentro de la carpeta de simulacros tenemos un archivo axios.js y esta es nuestra biblioteca axios falsa. Y dentro de nuestra biblioteca de axios falsos tenemos nuestra función simulacro de broma.

Las funciones simuladas nos permiten usar funciones en nuestro entorno de broma sin tener que implementar la lógica real de la función.

Básicamente, no vamos a implementar la lógica real detrás de una solicitud de obtención de axios. Solo usaremos esta función simulada en su lugar.

export default {
  get: jest.fn(() => Promise.resolve({ data: {} }) )
};

Aquí tenemos nuestra función de obtención falsa. Es una función simple que en realidad es un objeto JS. get es nuestra clave y el valor es la mock function. Como una solicitud de API de axios, resolvemos una promesa. No pasaremos ningún dato aquí, lo haremos en nuestra configuración de prueba.

Ahora nuestra configuración de prueba

//imports
...

afterEach(cleanup)

it('Async axios request works', async () => {
  axiosMock.get.mockResolvedValue({data: { title: 'some title' } })

  const url = 'https://jsonplaceholder.typicode.com/posts/1'
  const { getByText, getByTestId, rerender } = render(<TestAxios url={url} />);

  expect(getByText(/...Loading/i).textContent).toBe("...Loading")

  const resolvedEl = await waitForElement(() => getByTestId("title"));

  expect((resolvedEl).textContent).toBe("some title")

  expect(axiosMock.get).toHaveBeenCalledTimes(1);
  expect(axiosMock.get).toHaveBeenCalledWith(url);
 })

Lo primero que hacemos en nuestra prueba es llamar a nuestra falsa axios get request, y simular el valor resuelto con la función mockResolvedValue que ofrece jest. Esta función hace exactamente lo que dice su nombre, resuelve una promesa con los datos que le pasamos, lo que simula lo que hace axios.

Esta función debe llamarse antes de nuestra función render(), de lo contrario, la prueba no funcionará. Porque recuerda que nos estamos burlando de la propia biblioteca de axios. Cuando nuestro componente ejecuta la importación axios desde 'axios'; comando import axios from 'axios'; falsa en lugar de la real y esta axios falsa se sustituirá en nuestro componente donde sea que usemos axios.

A continuación, obtenemos nuestro nodo de texto "...Loading", ya que esto es lo que se mostrará antes de que se resuelva la promesa. Después de esto, tenemos una función que no hemos visto antes, la función waitForElement(), que esperará hasta que se resuelva la promesa antes de pasar a la siguiente afirmación.

Observa también las palabras clave await y async, que se usan exactamente de la misma manera que se usan en un entorno que no es de prueba.

Una vez resuelto, el nodo DOM tendrá el texto de "algún título", que son los datos que pasamos a nuestra biblioteca falsa de axios.

A continuación, nos aseguramos de que la solicitud solo se haya llamado una vez y con la URL correcta. Aunque estamos probando la URL, no hicimos una solicitud de API con esta URL.

Y esto es todo para las solicitudes de API con axios. En la siguiente sección veremos las pruebas e a e con ciprés.

Cypress

Ahora repasemos Cypress, que creo que es el mejor marco para ejecutar pruebas de e a e. Ahora estamos más tiempo en la tierra de las bromas, ahora trabajaremos únicamente con cypress, que tiene su propio entorno de prueba y sintaxis.

Cypress es bastante sorprendente y poderoso. De hecho, es tan asombroso y poderoso que podemos ejecutar todas las pruebas que acabamos de revisar en un bloque de prueba y ver cómo Cypress ejecuta estas pruebas en tiempo real en un navegador simulado.

Muy bien, ¿eh?

Creo que sí. De todos modos, antes de que podamos hacer eso, necesitamos configurar Cypress. Sorprendentemente, Cypress se puede instalar como un módulo npm normal.

npm install cypress

Para ejecutar cypress necesitará usar este comando.

node_modules/.bin/cypress open

Si eso parece engorroso de escribir cada vez que desee abrir cypress, puede agregarlo a su paquete.json.

...

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "cypress": "node_modules/.bin/cypress open", 
    
   ...

Esto le permitirá abrir cypress con solo el comando npm run cypress.

Abrir cypress te dará una GUI que se ve así.

image-14

Para ejecutar realmente las pruebas de Cypress, su aplicación deberá estar ejecutándose al mismo tiempo, lo que veremos en un segundo.

Ejecutar el comando cypress open le dará una configuración básica de cypress y creará algunos archivos y carpetas automáticamente. Se creará una carpeta Cypress en la raíz del proyecto. Escribiremos nuestro código en la carpeta de integración.

Podemos comenzar eliminando la carpeta de ejemplos. A diferencia de jest, los archivos de ciprés tienen una extensión .spec.js. Debido a que esta es una prueba de e a e, la ejecutaremos en nuestro archivo  App.js principal. Entonces debería tener una estructura de directorios que ahora se ve así.

image-21

También podemos establecer una URL base en el archivo cypress.json. Así como esto:

{ "baseUrl": "http://localhost:3000" }

Ahora para nuestra gran prueba monolítica

import React from 'react';

describe ('complete e to e test', () => {
  it('e to e test', () => {
    cy.visit('/')
    //counter test
    cy.contains("Clicked: 0")
      .click()
    cy.contains("Clicked: 1")
    // basic hooks test
    cy.contains("Initial State")
    cy.contains("State Change Button")
      .click()
    cy.contains("Initial State Changed")
    cy.contains("Moe")
    cy.contains("Change Name")
      .click()
    cy.contains("Steve")
    //useReducer test
    cy.contains('stateprop1 is false')
    cy.contains('Dispatch Success')
      .click()
    cy.contains('stateprop1 is true')
    //useContext test
    cy.contains("Some Text")
    cy.contains('Change Text')
      .click()
    cy.contains("Some Other Text")
    //form test
    cy.get('#text1')
      .type('New Text {enter}')
    cy.contains("Change: New Text")
    cy.contains("Submit Value: New Text")
    //axios test
    cy.request('https://jsonplaceholder.typicode.com/posts/1')
      .should(res => {
          expect(res.body).not.to.be.null
          cy.contains(res.body.title)
        })
  });
});

Como se mencionó, estamos ejecutando todas las pruebas que acabamos de revisar en un bloque de prueba. He separado cada sección con un comentario para que sea más fácil de ver.

Nuestra prueba puede parecer intimidante al principio, pero la mayoría de las pruebas individuales seguirán un patrón básico de arrange-act-assert pattern.


cy.contains(Some innerHTML text of DOM node)

cy.contains (text of button)
.click()

cy.contains(Updated innerHTML text of DOM node)

Dado que esta es una prueba de e a e, no encontrará ninguna burla. Nuestra aplicación se ejecutará en su versión de desarrollo completo en un navegador simulado con una interfaz de usuario. Esto será lo más cerca que podamos de probar nuestra aplicación de una manera realista.

A diferencia de las pruebas unitarias y de integración, no necesitamos afirmar explícitamente algunas cosas. Esto se debe a que algunos comandos de Cypress tienen afirmaciones predeterminadas integradas. Las aserciones predeterminadas son exactamente como suenan, se afirman de forma predeterminada, por lo que no es necesario agregar un comparador.

Aserciones predeterminadas de Cypress

Los comandos están encadenados, por lo que el orden es importante y un comando esperará hasta que se complete un comando anterior antes de ejecutarse.

Incluso al realizar pruebas con Cypress, nos apegaremos a nuestra filosofía de no probar los detalles de implementación. En la práctica, esto significará que no usaremos clases, ids o propiedades html/css como selectores si podemos evitarlo. La única vez que necesitaremos usar id es para obtener nuestro elemento de entrada de formulario.

Haremos uso del comando cy.contains() que devolverá un nodo DOM con texto coincidente. Ver e interactuar con el texto en la interfaz de usuario es lo que hará nuestro usuario final, por lo que probar de esta manera estará en línea con nuestro principio rector.

Dado que no estamos criticando ni burlándonos de nada, notará que nuestras pruebas se verán muy simples. Esto es bueno ya que se trata de una aplicación que se ejecuta en vivo, nuestras pruebas no tendrán valores artificiales.

En nuestra prueba de axios, haremos una solicitud http real a nuestro punto final. Hacer una solicitud http real en una prueba e to e es común. Luego comprobaremos si ese valor no es nulo. Luego, asegúrese de que los datos de la respuesta aparezcan en nuestra interfaz de usuario.

Si se hace correctamente, debería ver que Cypress ejecutó con éxito las pruebas en cromo.

image-22

Integración continua

Hacer un seguimiento y ejecutar todas estas pruebas manualmente puede volverse tedioso. Así que tenemos Integración Continua, una forma de ejecutar automáticamente nuestras pruebas de forma continua.

Travis CI

Para simplificar las cosas, solo usaremos Travis CI para nuestra integración continua. Sin embargo, debe saber que hay configuraciones de CI mucho más complejas que usan Docker y Jenkins.

Deberá registrarse para obtener una cuenta de Travis y Github, ambas son afortunadamente gratuitas.

Sugeriría simplemente usar la opción "Registrarse con Github" en Travis CI.

Una vez allí, puede ir al icono de su perfil y hacer clic en el botón deslizante junto al repositorio en el que desea CI.

image-15

Para que Travis CI sepa qué hacer, necesitaremos configurar un archivo .travis.yml en la raíz de nuestro proyecto.

language: node_js

node_js: 
  - stable
  
  
install:
  - npm install

script:
  - npm run test
  - npm run coveralls

Básicamente, esto le dice a Travis que estamos usando node_js, descargamos la versión estable, instalamos las dependencias y ejecutamos el comando npm run test y npm run coveralls.

Y esto es todo. Puede ir al panel de control e iniciar la compilación. Travis ejecutará las pruebas automáticamente y le dará un resultado como este. Si sus pruebas pasan, está listo para comenzar. Si fallan, su compilación fallará y deberá corregir su código y reiniciar la compilación.

image-16

Coveralls

Coverall nos brinda un informe de cobertura que esencialmente nos dice cuánto de nuestro código se está probando.

Deberás registrarte en overoles y sincronizar con tu cuenta de github. Similar a Travis CI, simplemente vaya a la pestaña Agregar repositorios y active el repositorio que también activó en Travis CI.

image-17

A continuación, ve a tu archivo package.json y agrega esta línea de código

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --coverage",
    "eject": "react-scripts eject",
    "cypress": "node_modules/.bin/cypress open", 
    "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls"
  },

Asegúrete de agregar el indicador --coverage al comando de prueba de react-scripts test. Esto es lo que generará los datos de cobertura que usará overoles para generar un informe de cobertura.

Y puedes ver estos datos de cobertura en la consola de Travis CI después de que se hayan ejecutado las pruebas.

image-18

Dado que no estamos tratando con un repositorio privado o Travis CI pro, no tenemos que preocuparnos por los tokens de repositorio.

Una vez que hayas terminado, puedes agregar una insignia a su repositorio README copiando el enlace provisto en el tablero.

image-19

Se verá así.

image-20

Conclusion

Cuéntate entre el 20% de los mejores desarrolladores en términos de habilidad de prueba de React si completó todo el tutorial.

Gracias por leer.

Puedes seguirme en twitter para más tutoriales en el futuro: https://twitter.com/iqbal125sf?lang=en