使用异步方法测试 React 组件
我有一个组件,其行为如下所示。
- 渲染,显示“正在加载”。
- 获取一些数据。
- 加载后,填充状态。
- 重新渲染,显示数据已加载。
代码如下:
import React from 'react';
class IpAddress extends React.Component {
state = {
ipAddress: null
};
constructor(props) {
super(props);
this.fetchData();
}
fetchData() {
return fetch(`https://jsonip.com`)
.then((response) => response.json())
.then((json) => {
this.setState({ ipAddress: json.ip });
});
}
render() {
if (!this.state.ipAddress) return <p class="Loading">Loading...</p>;
return <p>Pretty fly IP address you have there.</p>
}
}
export default IpAddress;
这很好用。但是 Jest 测试很费劲。使用 jest-fetch-mock 效果很好。
import React from 'react';
import ReactDOM from 'react-dom';
import { mount } from 'enzyme';
import IpAddress from './IpAddress';
it ('updates the text when an ip address has loaded', async () => {
fetch.mockResponse('{ "ip": "some-ip" }');
const address = mount(<IpAddress />);
await address.instance().fetchData();
address.update();
expect(address.text()).toEqual("Pretty fly IP address you have there.");
});
有点遗憾,我必须调用
await address.instance().fetchData()
,只是为了确保更新已经发生。如果没有这个,
fetch
的承诺或
setState
的异步特性(我不确定是哪个)直到我的
expect
之后才会运行;文本保留为“正在加载”。
这是测试此类代码的明智方法吗?您会完全不同地编写此代码吗?
我的问题已经升级。我正在使用
高阶组件
,这意味着我不能再执行
.instance()
并使用其上的方法 - 我不确定如何返回到我未包装的 IpAddress。使用
IpAddress.wrappedComponent
不会像我预期的那样返回原始 IpAddress。
此操作失败并显示以下错误消息,不幸的是我看不懂。
Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
Check the render method of `WrapperComponent`.
我必须承认我之前没有真正使用过 jest-fetch-mock,但从文档和我的小实验来看,它似乎用模拟版本替换了全局
fetch
。请注意,此示例中没有等待任何承诺:
https://github.com/jefflau/jest-fetch-mock#simple-mock-and-assert
。它只是检查
fetch
是否使用正确的参数调用。因此,我认为您可以删除 async/await 并断言存在对 jsonip.com 的调用。
我认为让您绊倒的实际上是 React 生命周期。本质上,它归结为您放置
fetch
调用的位置。 React 团队不鼓励您在
constructor
中放置“副作用”(如
fetch
)。这是官方 React 文档的描述:
https://reactjs.org/docs/react-component.html#constructor
。不幸的是,我还没有找到关于
为什么
的良好文档。我认为这是因为 React 可能会在生命周期中的奇怪时间调用
constructor
。我认为这也是您必须在测试中手动调用
fetchData
函数的原因。
放置副作用的最佳实践是在
componentDidMount
中。以下是原因的合理解释:
https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/
(尽管值得注意的是
componentWillMount
现在已在 React 16.2 中弃用)。
componentDidMount
仅在组件渲染到 DOM 后调用一次。
值得注意的是,随着 React 即将推出新版本,这一切将很快改变。这篇博文/会议视频详细介绍了以下内容: https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html
这种方法意味着它将在加载状态下最初渲染,但一旦请求得到解决,您可以通过设置状态来触发重新渲染。因为您在测试中使用了 Enzyme 的
mount
,所以这将调用所有必要的生命周期方法,包括
componentDidMount
,因此您应该看到模拟的
fetch
被调用。
至于高阶组件,我有时会使用一个技巧,虽然这可能不是最佳实践,但我认为这是一个非常有用的技巧。ES6 模块有一个
default
导出,以及任意数量的“常规”导出。我利用这一点多次导出组件。
React 惯例是在导入组件时使用
default
导出(即
import MyComponent from './my-component'
)。这意味着您仍然可以从文件中导出其他内容。
我的技巧是
export default
HOC 包装的组件,以便您可以像使用任何其他组件一样在源文件中使用它,但
也
将未包装的组件导出为“常规”组件。它看起来像:
export class MyComponent extends React.Component {
...
}
export default myHOCFunction()(MyComponent)
然后,您可以使用以下方式导入包装的组件:
import MyComponent from './my-component'
以及使用以下方式导入未包装的组件(即用于测试):
import { MyComponent } from './my-component'
这不是世界上最明确的模式,但在我看来它非常符合人体工程学。如果您想要明确,您可以执行以下操作:
export const WrappedMyComponent = myHOCFunction()(MyComponent)
export const UnwrappedMyComponent = MyComponent
您可以使用
react-testing-library
的
waitForElement
来避免在
fetch
调用中显式地
await
,并稍微简化一些事情:
import React from "react";
import IpAddress from "./IpAddress";
import { render, cleanup, waitForElement } from "react-testing-library";
// So we can use `toHaveTextContent` in our expectations.
import "jest-dom/extend-expect";
describe("IpAddress", () => {
beforeEach(() => {
fetch.resetMocks();
});
afterEach(cleanup);
it("updates the text when an IP address has loaded", async () => {
fetch.mockResponseOnce(JSON.stringify({ ip: "1.2.3.4" }));
const { getByTestId } = render(<IpAddress />);
// If you add data-testid="ip" to your <p> in the component.
const ipNode = await waitForElement(() => getByTestId("ip"));
expect(ipNode).toHaveTextContent("Pretty fly IP address you have there.");
});
});
这将自动等待您的元素出现,如果在某个超时时间内没有出现,则会失败。您仍然必须
await
,但希望这更接近您最初想要的。