Jest.js 我应该如何测试React Hook“useEffect”使用Typescript进行API调用?

dkqlctbz  于 2023-06-20  发布在  Jest
关注(0)|答案(3)|浏览(209)

我正在使用Typescript和新的React hooks为一个简单的React应用程序编写一些jest-enzyme测试。
但是,我似乎无法正确模拟useEffect钩子内部的API调用。
useEffect进行API调用并使用“setData”更新useState状态“data”。
对象“data”然后被Map到表中到其对应的表单元格。
这看起来应该很容易用模拟的API响应和酶挂载来解决,但我不断收到错误,告诉我使用act()进行组件更新。
我尝试了很多方法来使用act(),但都没有用。我试过用fetch替换axios,并使用enzyme shallow和react-test-library的render,但似乎都不起作用。

组件:

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(...).中,我找了很多小时的解决方案,但找不到任何有效的解决方案,所以我决定鼓起勇气在这里询问。

kzmpq1sx

kzmpq1sx1#

这里有两个问题

异步调用setData

setDataPromise回调中被调用。
一旦Promise解析,等待它的任何回调都会在PromiseJobs队列中排队。PromiseJobs队列中的任何挂起作业都将 * 在当前消息完成之后、下一个消息开始之前 * 运行。
在这种情况下,当前正在运行的消息是您的测试,因此您的测试在Promise回调有机会运行之前完成,并且setData直到您的测试完成之后才被调用。
可以通过使用setImmediate之类的东西来解决这个问题,将Assert延迟到PromiseJobs中的回调有机会运行之后。
看起来您还需要调用component.update()来重新呈现具有新状态的组件。(我猜这是因为状态更改发生在act之外,因为没有任何方法可以将回调代码 Package 在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外部发生的组件状态更新触发。

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,谈论了同样的行为。
我在评论中添加了上述测试,以帮助enzyme开发人员解决这个问题。

pftdvrlh

pftdvrlh2#

解决方案

它既能工作,又能摆脱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...
})

感谢...
这是edpark 11issue@Brian_亚当斯的answer中提出的一个解决方案。
原文:https://github.com/enzymejs/enzyme/issues/2073#issuecomment-565736674
我把这篇文章复制到这里,为了存档起见做了一些修改。

whhtz7ly

whhtz7ly3#

通常,模仿用于发出获取请求的库是一种不好的做法。假设你想用fetch或isomorphic-unfetch替换axios?您必须用一个新的模拟来完全替换测试套件中的所有模拟。将测试绑定到服务器契约比绑定到模拟更好。
使用服务器存根库,如mswnock + React Testing Library(RTL)。RTL有一些很棒的工具来在异步执行上踢React的生命周期。
下面是我将如何使用您提供的示例重写测试:

RTL + Nock

/* SalesPlanTable.jsx */

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);
  const [status, setStatus] = useState('loading');

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('/path/to/api');
        setData(response.data.results);
        setStatus('ready');
      } catch (error) {
        console.log(error);
        setStatus('error');
      }
    };

    fetchData();
  }, []);

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

  if (status === 'loading') {
    return <div>Loading...</div>;
  }

  if (status === 'error') {
    return <div>Error occurred while fetching data.</div>;
  }

  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;
/* SalesPlanTable.test.jsx */

import { render, screen } from '@testing-library/react';
import nock from 'nock';

import SalesPlanTable from './SalesPlanTable';

/**
 * @NOTE: This should probably go into a `__fixtures__` folder.
 */
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',
      },
    ],
  },
};

describe('<SalesPlanTable />', () => {
  it('displays the title', async () => {
    const scope = nock('http://localhost')
      .get('/path/to/api')
      .reply(200, response.data);

    render(<SalesPlanTable />);
  
    // Wait for the async task to kick over
    await waitFor(() => {
      expect(screen.getByText('Loading...')).not.toBeInTheDocument();
    });
  
    // Test the render
    expect(screen.getByText('ELLE')).toBeInTheDocument();
    expect(scope.isDone()).toBeTruthy();
  });
});

酶+ Nock

/* SalesPlanTable.jsx */

import React from 'react';
import { mount } from 'enzyme';
import nock from 'nock';

import SalesPlanTable from './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',
      },
    ],
  },
};

describe('<SalesPlanTable />', () => {
  it('displays the title', async () => {
    nock('http://localhost')
      .get('/path/to/api')
      .reply(200, response.data);

    const component = mount(<SalesPlanTable />);
    
    // Wait for API call to complete
    await new Promise((resolve) => setTimeout(resolve)); 
    component.update();

    expect(component.find('td').at(1).text()).toBe('ELLE');
    expect(scope.isDone()).toBeTruthy();
  });
});

相关问题