Búsqueda de sitios web

jestjs - ¿Cómo debería probar el Hook de React "useEffect" que realiza una llamada a la API con Typescript?


Estoy escribiendo algunas pruebas con jest-enzyme para una aplicación simple de React usando Typescript y los nuevos hooks de React.

Sin embargo, no puedo parecer simular correctamente la llamada a la API que se realiza dentro del hook useEffect.

useEffect realiza la llamada a la API y actualiza el estado useState "data" con "setData".

El objeto "data" se asigna luego a una tabla en sus correspondientes celdas de la tabla.

Esto parece que debería ser fácil de abordar con una respuesta de API simulada y un montaje con enzyme, pero sigo recibiendo errores que me dicen que use act() para las actualizaciones del componente.

Intenté usar act() de muchas maneras, pero en vano. He intentado reemplazar axios con fetch y usar shallow de enzyme y render de react-test-library, pero nada parece funcionar.

El componente:

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

interface ISUB {
  id: number;
  mediaType: {
    digital: boolean;
    print: boolean;
  };
  monthlyPayment: {
    digital: boolean;
    print: boolean;
  };
  singleIssue: {
    digital: boolean;
    print: boolean;
  };
  subscription: {
    digital: boolean;
    print: boolean;
  };
  title: string;
}

interface IDATA extends Array<ISUB> {}

const initData: IDATA = [];

const SalesPlanTable = () => {
  const [data, setData] = useState(initData);
  useEffect(() => {
    axios
      .get(`/path/to/api`)
      .then(res => {
        setData(res.data.results);
      })
      .catch(error => console.log(error));
  }, []);

  const renderTableRows = () => {
    return data.map((i: ISUB, k: number) => (
      <tr key={k}>
        <td>{i.id}</td>
        <td>
          {i.title}
        </td>
        <td>
          {i.subscription.print}
          {i.mediaType.digital}
        </td>
        <td>
          {i.monthlyPayment.print}
          {i.monthlyPayment.digital}
        </td>
        <td>
          {i.singleIssue.print}
          {i.singleIssue.digital}
        </td>
        <td>
          <button>Submit</button>
        </td>
      </tr>
    ));
  };

  return (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>MediaType</th>
          <th>MonthlyPayment</th>
          <th>SingleIssue</th>
          <th/>
        </tr>
      </thead>
      <tbody'>{renderTableRows()}</tbody>
    </table>
  );
};

export default SalesPlanTable;

La prueba:

const response = {
  data: {
    results: [
      {
        id: 249,
        mediaType: {
          digital: true,
          print: true
        },
        monthlyPayment: {
          digital: true,
          print: true
        },
        singleIssue: {
          digital: true,
          print: true
        },
        subscription: {
          digital: true,
          print: true
        },
        title: 'ELLE'
      }
    ]
  }
};

//after describe

it('should render a proper table data', () => {
    const mock = new MockAdapter(axios);
    mock.onGet('/path/to/api').reply(200, response.data);
    act(() => {
      component = mount(<SalesPlanTable />);
    })
    console.log(component.debug())
  });

Espero que registre el HTML de la tabla con la sección del cuerpo de la tabla renderizada. Probé con async y diferentes formas de simular axios, pero sigo obteniendo solo los encabezados de la tabla o el mensaje: Una actualización de SalesPlanTable dentro de una prueba no fue envuelta en act(...). Busqué durante muchas horas una solución pero no puedo encontrar nada que funcione, así que decidí reunir un poco de valor y preguntar aquí.

Respuesta #1

Hay dos problemas en juego aquí

Llamada asincrónica a setData

setData se llama en un callback de Promise.

Tan pronto como se resuelve un Promise, cualquier callback que esté esperando se coloca en la cola de PromiseJobs. Cualquier trabajo pendiente en la cola de PromiseJobs se ejecuta después de que el mensaje actual haya finalizado y antes de que comience el siguiente.

En este caso, el mensaje que se está ejecutando actualmente es tu prueba, por lo que tu prueba se completa antes de que el callback de Promise tenga la oportunidad de ejecutarse y setData no se llama hasta después de que tu prueba se complete.

Puedes solucionar esto usando algo como setImmediate para retrasar tus afirmaciones hasta que los callbacks en PromiseJobs tengan la oportunidad de ejecutarse.

Parece que también necesitarás llamar a component.update() para volver a renderizar el componente con el nuevo estado. (Supongo que esto se debe a que el cambio de estado ocurre fuera de un act ya que no hay manera de envolver ese código de devolución de llamada en un act.)

En conjunto, la prueba de funcionamiento se ve así:

it('should render a proper table data', done => {
  const mock = new MockAdapter(axios);
  mock.onGet('/path/to/api').reply(200, response.data);
  const component = mount(<SalesPlanTable />);
  setImmediate(() => {
    component.update();
    console.log(component.debug());
    done();
  });
});

Advertencia: Una actualización a ... no fue envuelta en act(...)

La advertencia se activa por actualizaciones de estado en el componente que ocurren fuera de un act.

Los cambios de estado causados por llamadas asincrónicas a setData desencadenadas por una función useEffect siempre ocurrirán fuera de un act.

Aquí hay una prueba extremadamente simple que demuestra este comportamiento:

import React, { useState, useEffect } from 'react';
import { mount } from 'enzyme';

const SimpleComponent = () => {
  const [data, setData] = useState('initial');

  useEffect(() => {
    setImmediate(() => setData('updated'));
  }, []);

  return (<div>{data}</div>);
};

test('SimpleComponent', done => {
  const wrapper = mount(<SimpleComponent/>);
  setImmediate(done);
});

Mientras buscaba más información, me topé con el problema #2073 de enzyme abierto hace apenas 10 horas que habla de este mismo comportamiento.

Agregué la prueba anterior en un comentario para ayudar a los desarrolladores de enzyme a abordar el problema.

Respuesta #2

Solución

Funciona y elimina la advertencia test was not wrapped in act(...).

const waitForComponentToPaint = async (wrapper) => {
   await act(async () => {
     await new Promise(resolve => setTimeout(resolve, 0));
     wrapper.update();
   });
};

Uso :

it('should do something', () => {
    const wrapper  = mount(<MyComponent ... />);
    await waitForComponentToPaint(wrapper);
    expect(wrapper).toBlah...
})

Gracias a...

Esta es una solución alternativa sugerida por edpark11 en el problema que @Brian_Adams mencionó en su respuesta.

Publicación original: https://github.com/enzymejs/enzyme/issues/2073#issuecomment-565736674

Copié la publicación aquí con algunas modificaciones para fines de archivo.

Artículos relacionados