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

时间:2019-03-28 00:48:18

标签: typescript jestjs axios enzyme react-hooks

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

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

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

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

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

我尝试了多种方式使用act(),但无济于事。我尝试用获取代替axios并使用酶浅和反应测试库的渲染,但是似乎没有任何作用。

组件:

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,但是我一直只是获取表头或消息: SalesPlanTable中的act(...).并没有被{{1}}包裹,我花了好几个小时来寻找解决方案,但找不到任何可行的方法,因此我决定鼓起勇气,在这里提问。

3 个答案:

答案 0 :(得分:2)

这里有两个问题


异步调用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外部发生的组件状态更新触发的。

setData函数触发的异步调用useEffect导致的状态更改总是会在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开发人员解决此问题。

答案 1 :(得分:1)

解决方案

它既有效,又摆脱警告{

test was not wrapped in act(...)

用法:

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

感谢...

这是 edpark11 在其issue中提到的answer @Brian_Adams中建议的解决方法。

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

为便于存档,我在此处复制了帖子,并进行了一些修改。

答案 2 :(得分:0)

我尝试了上面列出的解决方案:

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

我在expect之后有setImmediate

expect(component.html()).toContain("my text"); 

我发现console.log(component.debug())中的setImmediate具有正确的文本。但是component.html()并不符合我的期望,我的期望也失败了。

这是为什么?

编辑:

尽管我不明白为什么,但是我可以通过使用beforeEach来解决问题。

describe('Hello', () => {

    beforeEach(() => {

      const mockAxios = new MockAdapter(axios);
      const mockData : HelloData = { Message: "mock Hello, world"};

      mockAxios.onGet(helloUrl).reply(200, mockData);
      component =  mount(<HelloAxios />);

    })

    it(' using axios should contain "mock Hello, world"', () => {
      expect(component.html()).toContain("mock Hello, world");
    });
});