我应该如何测试 React Hook "useEffect" 使用 Typescript 进行 api 调用?

How should I test React Hook "useEffect" making an api call with Typescript?

我正在使用 Typescript 和新的 React hooks 为一个简单的 React 应用编写一些 jest-enzyme 测试。

但是,我似乎无法正确模拟在 useEffect 挂钩中进行的 api 调用。

useEffect 调用 api 并用 "setData" 更新 useState 状态 "data"。

然后 object "data" 映射到 table 对应的 table 单元格。

这似乎应该很容易通过模拟 api 响应和酶安装来解决,但我一直收到错误提示我使用 act() 进行组件更新。

我尝试使用 act() 多种方法,但无济于事。我尝试用 fetch 替换 axios 并使用 enzyme shallow 和 react-test-library 的渲染器,但似乎没有任何效果。

组件:

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;

测试:

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())
  });

我希望它记录 table 的 html 和呈现的 table body 部分,我尝试了一些异步和不同的方法来模拟 axios 但我要么只得到 table headers 要么得到消息:测试中对 SalesPlanTable 的更新没有包含在 act(...). 中 我找了很多小时分辨率,但找不到任何有用的东西,所以我决定鼓起勇气在这里问一下。

这里有两个问题


异步调用 setData

setDataPromise 回调中被调用。

一旦 Promise 解析,任何等待它的回调都会在 PromiseJobs queue 中排队。 PromiseJobs 队列中的任何待处理作业 运行 在当前消息完成之后和下一个消息开始之前

在这种情况下,当前 运行ning 消息是您的测试,因此您的测试在 Promise 回调有机会 运行 而 setData 没有之前完成在您的测试完成 之后调用。

您可以通过使用 setImmediate 之类的东西来解决此问题,以延迟您的断言,直到 PromiseJobs 中的回调有机会 运行.

看来您还需要调用 component.update() 以使用新状态重新呈现组件。 (我猜这是因为状态更改发生在 act 之外,因为没有任何方法可以将该回调代码包装在 act 中。)

总的来说,工作测试看起来像这样:

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();
  });
});

警告:对...的更新未包含在 act(...)

警告是由 act 之外发生的组件状态更新触发的。

useEffect 函数 触发的对 setData 的异步调用引起的状态更改将始终发生在 act 之外。

这是一个非常简单的测试来演示这种行为:

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);
});

当我在搜索更多信息时,我偶然发现了 enzyme issue #2073 10 小时前才开张,谈论同样的行为。

我添加了上面的测试 in a comment 来帮助 enzyme 开发者解决这个问题。

解决方案

有效 并且 摆脱了 test was not wrapped in act(...) 警告。

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

用法:

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

感谢...

这是 edpark11issue @Brian_Adams mentioned in his .

中建议的 work-around

原文post:https://github.com/enzymejs/enzyme/issues/2073#issuecomment-565736674

为了存档,我复制了这里的 post 并做了一些修改。