我有一个行为如下的组件。
代码是这样的:
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
之后才会运行;文本保留为“正在加载”。
这是测试这样的代码的理智方式吗?你会以完全不同的方式编写这段代码吗?
我的问题已经升级了。我正在使用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`.
答案 0 :(得分:1)
我必须承认以前没有使用真正的jest-fetch-mock,但是从文档和我的小实验中,看起来它用模拟版本替换了全局fetch
。请注意,此示例中没有等待任何承诺:https://github.com/jefflau/jest-fetch-mock#simple-mock-and-assert。它只是检查用正确的参数调用fetch
。因此,我认为您可以删除async / await并断言调用jsonip.com。
我认为绊倒你实际上是React的生命周期。从本质上讲,它归结为fetch
调用的位置。 React团队不鼓励您在fetch
中加入“副作用”(如constructor
)。以下是官方的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中已被弃用)。仅在将组件呈现到DOM中之后,才会调用componentDidMount
一次。
值得注意的是,即将推出的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
答案 1 :(得分:1)
您可以使用react-testing-library的waitForElement
来避免在您的await
通话中显式地fetch
并简化一些事情:
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
,但希望这与您最初想要的有点接近。