我有一个组件,其行为如下。
- 渲染,显示“正在加载”。
- 获取一些数据。
- 加载后,填充状态。
- 重新渲染,显示数据已加载。
代码是这样的:
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
的 promise 或 setState
的异步性质(我不确定是哪一个)直到我的 expect
之后才会运行;文本保留为“正在加载”。
这是测试这样的代码的明智方法吗?您会以完全不同的方式编写这段代码吗?
此后我的问题升级了。我正在使用 high order component ,这意味着我无法再执行 .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
。请注意在此示例中如何不等待任何 promise :https://github.com/jefflau/jest-fetch-mock#simple-mock-and-assert 。它只是检查是否使用正确的参数调用了 fetch 。因此,我认为您可以删除 async/await 并断言存在对 jsonip.com 的调用。
我认为让你困惑的实际上是 React 生命周期。本质上,它归结为您放置 fetch
调用的位置。 React 团队不鼓励您在构造函数
中添加“副作用”(例如fetch
)。这是官方 React 文档描述:https://reactjs.org/docs/react-component.html#constructor 。不幸的是,我还没有真正找到关于原因的良好文档。我相信这是因为 React 可能会在生命周期中的奇怪时间调用构造函数。我认为这也是您必须在测试中手动调用 fetchData
函数的原因。
放置副作用的最佳实践位于 componentDidMount
。这是原因的一个很好的解释:https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/ (尽管值得注意的是,componentWillMount
现在在 React 16.2 中已被弃用)。 componentDidMount
仅在组件渲染到 DOM 后调用一次。
还值得注意的是,随着即将推出的 React 版本,这一切都将很快改变。这篇博文/ session 视频介绍了更多细节: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'
)。这意味着您仍然可以从文件中导出其他内容。
我的技巧是导出默认
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
关于javascript - 使用异步方法测试 React 组件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50320038/