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

Posted

技术标签:

【中文标题】我应该如何测试使用 Typescript 进行 api 调用的 React Hook “useEffect”?【英文标题】:How should I test React Hook "useEffect" making an api call with Typescript? 【发布时间】:2019-08-18 16:36:42 【问题描述】:

我正在使用 Typescript 和新的 React 钩子为一个简单的 React 应用程序编写一些玩笑酶测试。

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

useEffect 进行 api 调用并使用“setData”更新useState 状态“data”。

然后将对象“数据”映射到一个表格中到其对应的表格单元格。

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

我尝试了多种方式使用act(),但无济于事。我尝试用 fetch 替换 axios 并使用酶浅层和 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())
  );

我希望它记录表格的 html 并呈现表格正文部分,我尝试了一些异步和不同的方法来模拟 axios,但我一直只得到表格标题或消息:@987654330 的更新@ inside a test is not Wrap in act(...). 我找了好几个小时才找到解决办法,但找不到任何可行的方法,所以我决定鼓起勇气在这里问。

【问题讨论】:

so I decided to muster up some courage and ask here ...我们没那么可怕吧? :)(好问题,顺便说一句) 哈哈我前段时间在这里问过一些东西,但我对 javascript 的经验不够丰富,在问之前没有彻底搜索过这里,所以它立即被删除了:( 【参考方案1】:

这里有两个问题


异步调用setData

setDataPromise 回调中被调用。

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

在这种情况下,当前正在运行的消息是您的测试,因此您的测试在 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);
);

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

我添加了上述测试in a comment 以帮助enzyme 开发人员解决问题。

【讨论】:

非常感谢!!它仍然像你说的那样有奇怪的“act()”警告,但表格现在实际上呈现了正确的数据,这就是我所需要的......向你致敬,先生! 使用jest.useFakeTimers 并将jest.runAllImmediates 包裹在act 中。这样就不会再出现错误:github.com/eps1lon/react-act-immediate/blob/…。不过,我无法用这种方法解决原来的问题。 测试通过,但仍然得到Warning: An update to Post inside a test was not wrapped in act(...). 我可以通过将 setImmediate 包装在 act 中来删除警告,如下所示:await act(() =&gt; new Promise&lt;void&gt;((resolve) =&gt; setImmediate(() =&gt; node.update(); resolve(); ); ))【参考方案2】:

解决方案

它既有效摆脱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...
)

感谢...

这是 edpark11 在issue @Brian_Adams 在他的answer 中提到的一种解决方法。

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

为了存档,我复制了这里的帖子并进行了一些修改。

【讨论】:

以上是关于我应该如何测试使用 Typescript 进行 api 调用的 React Hook “useEffect”?的主要内容,如果未能解决你的问题,请参考以下文章

如何测试使用 jasmine + TypeScript 使用常量调用的函数

如何在 Typescript 中对私有方法进行单元测试

如何允许 scss 开玩笑地对 typescript nextjs 进行单元测试?

使用 typescript/mocha 进行单元测试时找不到模块

如何在Typescript中对私有方法进行单元测试

Jasmine 使用 Angular 中的 TypeScript 对文件大小进行单元测试