我正在使用Typescript和新的React钩子为一个简单的React应用程序编写一些jest-enzyme测试。
但是,我似乎无法正确模拟在useEffect
钩子中进行的 api 调用。
useEffect
进行 API 调用,并使用"setData"更新useState
状态"data"。
然后将对象"data"映射到表中到其相应的表单元格。
这似乎很容易通过模拟的 api 响应和酶安装来解决,但我不断收到错误,告诉我使用act()
进行组件更新。
我尝试使用act()
多种方法,但无济于事。我尝试用 fetch 替换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(...).
中 我寻找了很多小时的解决方案,但找不到任何有效的方法,所以我决定鼓起勇气并在这里询问。
这里有两个问题在起作用
对setData
的异步调用
setData
在Promise
回调中被调用。
一旦Promise
解析,任何等待它的回调都会在 PromiseJobs 队列中排队。 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
之外发生的组件状态更新触发的。
由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
我偶然发现了 2073 小时前打开的问题 #10,谈论同样的行为。
我在评论中添加了上述测试,以帮助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...
})
由于。。。
这是edpark11在他的回答中提到的问题@Brian_Adams建议的解决方法。
原文: https://github.com/enzymejs/enzyme/issues/2073#issuecomment-565736674
为了存档起见,我在这里复制了这篇文章并进行了一些修改。
模拟用于发出获取请求的库通常是一种不好的做法。假设您想用 fetch 或同构取消获取替换 axios?您必须将测试套件中的所有模拟完全替换为新的模拟。最好将测试绑定到服务器合约而不是模拟。
使用服务器存根库,如 msw 或 nock + React 测试库 (RTL)。RTL 有一些很棒的工具可以在异步执行上启动 React 的生命周期。
以下是我如何使用您提供的示例重写测试:
劳特 + 诺克
/* 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();
});
});
酶+诺克
/* 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();
});
});