绕过 React 组件中的获取调用
Bypass fetch call in React component
我正在尝试为一个在挂载后发出获取 GET 请求的 React 组件编写测试。测试只需要检查组件是否已呈现,以及当 fetch 运行时我得到这个:ReferenceError: fetch is not defined
。我四处搜寻,似乎找不到任何可以解决我的问题的方法。我正在使用 jest 和 Test Utils 来测试组件。
我的组件代码:
export class Home extends Component {
constructor(props) {
...
}
componentDidMount() {
fetch('/some/path', {
headers: {
'Key1': 'Data1',
'Key2': Data2
}
}).then(response => {
if (response.status == 200) {
response.json().then((data) => {
this.context.store.dispatch(setAssets(data))
}
);
} else {
return (
<Snackbar
open={true}
message={"ERROR: " + str(response.status)}
autoHideDuration={5000}
/>
);
}
}).catch(e => {});
...
);
}
componentWillUnmount() {
...
}
logout(e) {
...
}
render() {
return (
<div>
<AppBar
title="Title"
iconElementLeft={
<IconButton>
<NavigationClose />
</IconButton>
}
iconElementRight={
<IconMenu
iconButtonElement={
<IconButton>
<MoreVertIcon />
</IconButton>
}
targetOrigin={{
horizontal: 'right',
vertical: 'top'
}}
anchorOrigin={{
horizontal: 'right',
vertical: 'top'
}}
>
<MenuItem>
Help
</MenuItem>
</IconMenu>
}
/>
{
this.context.store.getState().assets.map((asset, i) => {
return (
<Card
title={asset.title}
key={i+1}
/>
);
})
}
</div>
);
}
}
Home.contextTypes = {
store: React.PropTypes.object
}
export default Home;
我的测试代码:
var home
describe('Home', () => {
beforeEach(() => {
let store = createStore(assets);
let a = store.dispatch({
type: Asset,
assets: [{
'id': 1,
'title': 'TITLE'
}],
});
store.getState().assets = a.assets
home = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Provider store={store}>
<Home />
</Provider>
</MuiThemeProvider>
);
});
it('renders the main page, including cards and appbar', () => {}
尝试将主页渲染到文档时出错。
我已经尝试 fetch-mock 但这只允许对 API 测试进行模拟调用,我没有尝试这样做,也不会在我的组件中模拟获取调用。
Mocking Home 无法工作,因为它是我正在尝试测试的组件。除非有办法模拟我错过的 componentDidMount()
函数。
我只需要获取调用的解决方法。有什么想法吗??
编辑:我使用 React 的 JSX 作为组件,使用 JS 作为测试
尝试 https://github.com/jhnns/rewire:
rewire adds a special setter and getter to modules so you can modify
their behaviour for better unit testing
var fetchMock = { ... }
var rewire = require("rewire");
var myComponent = rewire("./myComponent.js");
myComponent.__set__("fetch", fetchMock);
不幸的是,我正在使用 babel,它被列为 limitation for rewire,但我还是试过了...
我添加了:
...
store.getState().assets = a.assets
var fetchMock = function() {return '', 200}
var rewire = require("rewire");
var HomeComponent = rewire('../Home.jsx');
HomeComponent.__set__("fetch", fetchMock);
home = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Provider store={store}>
<Home />
...
并收到错误:
Error: Cannot find module '../Home.jsx'
at Function.Module._resolveFilename (module.js:440:15)
at internalRewire (node_modules/rewire/lib/rewire.js:23:25)
at rewire (node_modules/rewire/lib/index.js:11:12)
at Object.eval (Some/Path/Home-test.js:47:21)
我假设这是因为 babel:
rename[s] variables in order to emulate certain language features. Rewire will not work in these cases
(从 link chardy 提供给我)
但是路径不是变量,所以我想知道 babel 是否真的在重命名它,并且路径对于我的组件位置是 100% 正确的。我不认为这是因为我正在使用 JSX,因为它只是找不到组件,这不是兼容性问题……不幸的是,即使它找到了文件,Rewire 仍然可能无法工作,但我想给还是一枪。
我找到了一个对我有用的答案,它非常简单,没有包含任何其他依赖项。很简单,就是将主函数存储到一个变量中并覆盖它,然后在测试用例之后恢复正确的函数
解决方案:
var home
describe('Home', () => {
const fetch = global.fetch
beforeEach(() => {
let store = createStore(assets);
let a = store.dispatch({
type: Asset,
assets: [{
'id': 1,
'title': 'TITLE'
}],
});
store.getState().assets = a.assets
global.fetch = () => {return Promise.resolve('', 200)}
home = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Provider store={store}>
<Home />
</Provider>
</MuiThemeProvider>
);
});
it('renders the main page, including cards and appbar', () => {
...
});
afterEach(() => {
global.fetch = fetch;
});
});
使用全局上下文来存储组件可能很脆弱,对于任何大型项目来说可能都不是一个好主意。
相反,您可以使用依赖项注入 (DI) 模式,这是一种更正式的方法,用于根据您的运行时配置切换不同的组件依赖项(在本例中为获取)。
https://en.wikipedia.org/wiki/Dependency_injection
使用 DI 的一种简洁方法是使用控制反转 (IoC) 容器,例如:
https://github.com/jaredhanson/electrolyte
我所做的是将 fetch
调用从组件移到存储库 class 中,然后从组件内部调用它。这样组件就独立于数据源,我可以将存储库切换为虚拟存储库,以便测试或更改实现,从执行获取到从 localStorage
、sessionStorage
获取数据IndexedDB、文件系统等
class Repository {
constructor() {
this.url = 'https://api.example.com/api/';
}
getUsers() {
return fetch(this.url + 'users/').then(this._handleResponse);
}
_handleResponse(response) {
const contentType = response.headers.get('Content-Type');
const isJSON = (contentType && contentType.includes('application/json')) || false;
if (response.ok && isJSON) {
return response.text();
}
if (response.status === 400 && isJSON) {
return response.text().then(x => Promise.reject(new ModelStateError(response.status, x)));
}
return Promise.reject(new Error(response.status));
}
}
class ModelStateError extends Error {
constructor(message, data) {
super(message);
this.name = 'ModelStateError';
this.data = data;
}
data() { return this.data; }
}
用法:
const repository = new Repository();
repository.getUsers().then(
x => console.log('success', x),
x => console.error('fail', x)
);
示例:
export class Welcome extends React.Component {
componentDidMount() {
const repository = new Repository();
repository.getUsers().then(
x => console.log('success', x),
x => console.error('fail', x)
);
}
render() {
return <h1>Hello!</h1>;
}
}
我正在尝试为一个在挂载后发出获取 GET 请求的 React 组件编写测试。测试只需要检查组件是否已呈现,以及当 fetch 运行时我得到这个:ReferenceError: fetch is not defined
。我四处搜寻,似乎找不到任何可以解决我的问题的方法。我正在使用 jest 和 Test Utils 来测试组件。
我的组件代码:
export class Home extends Component {
constructor(props) {
...
}
componentDidMount() {
fetch('/some/path', {
headers: {
'Key1': 'Data1',
'Key2': Data2
}
}).then(response => {
if (response.status == 200) {
response.json().then((data) => {
this.context.store.dispatch(setAssets(data))
}
);
} else {
return (
<Snackbar
open={true}
message={"ERROR: " + str(response.status)}
autoHideDuration={5000}
/>
);
}
}).catch(e => {});
...
);
}
componentWillUnmount() {
...
}
logout(e) {
...
}
render() {
return (
<div>
<AppBar
title="Title"
iconElementLeft={
<IconButton>
<NavigationClose />
</IconButton>
}
iconElementRight={
<IconMenu
iconButtonElement={
<IconButton>
<MoreVertIcon />
</IconButton>
}
targetOrigin={{
horizontal: 'right',
vertical: 'top'
}}
anchorOrigin={{
horizontal: 'right',
vertical: 'top'
}}
>
<MenuItem>
Help
</MenuItem>
</IconMenu>
}
/>
{
this.context.store.getState().assets.map((asset, i) => {
return (
<Card
title={asset.title}
key={i+1}
/>
);
})
}
</div>
);
}
}
Home.contextTypes = {
store: React.PropTypes.object
}
export default Home;
我的测试代码:
var home
describe('Home', () => {
beforeEach(() => {
let store = createStore(assets);
let a = store.dispatch({
type: Asset,
assets: [{
'id': 1,
'title': 'TITLE'
}],
});
store.getState().assets = a.assets
home = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Provider store={store}>
<Home />
</Provider>
</MuiThemeProvider>
);
});
it('renders the main page, including cards and appbar', () => {}
尝试将主页渲染到文档时出错。
我已经尝试 fetch-mock 但这只允许对 API 测试进行模拟调用,我没有尝试这样做,也不会在我的组件中模拟获取调用。
Mocking Home 无法工作,因为它是我正在尝试测试的组件。除非有办法模拟我错过的 componentDidMount()
函数。
我只需要获取调用的解决方法。有什么想法吗??
编辑:我使用 React 的 JSX 作为组件,使用 JS 作为测试
尝试 https://github.com/jhnns/rewire:
rewire adds a special setter and getter to modules so you can modify their behaviour for better unit testing
var fetchMock = { ... }
var rewire = require("rewire");
var myComponent = rewire("./myComponent.js");
myComponent.__set__("fetch", fetchMock);
不幸的是,我正在使用 babel,它被列为 limitation for rewire,但我还是试过了...
我添加了:
...
store.getState().assets = a.assets
var fetchMock = function() {return '', 200}
var rewire = require("rewire");
var HomeComponent = rewire('../Home.jsx');
HomeComponent.__set__("fetch", fetchMock);
home = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Provider store={store}>
<Home />
...
并收到错误:
Error: Cannot find module '../Home.jsx'
at Function.Module._resolveFilename (module.js:440:15)
at internalRewire (node_modules/rewire/lib/rewire.js:23:25)
at rewire (node_modules/rewire/lib/index.js:11:12)
at Object.eval (Some/Path/Home-test.js:47:21)
我假设这是因为 babel:
rename[s] variables in order to emulate certain language features. Rewire will not work in these cases
(从 link chardy 提供给我)
但是路径不是变量,所以我想知道 babel 是否真的在重命名它,并且路径对于我的组件位置是 100% 正确的。我不认为这是因为我正在使用 JSX,因为它只是找不到组件,这不是兼容性问题……不幸的是,即使它找到了文件,Rewire 仍然可能无法工作,但我想给还是一枪。
我找到了一个对我有用的答案,它非常简单,没有包含任何其他依赖项。很简单,就是将主函数存储到一个变量中并覆盖它,然后在测试用例之后恢复正确的函数
解决方案:
var home
describe('Home', () => {
const fetch = global.fetch
beforeEach(() => {
let store = createStore(assets);
let a = store.dispatch({
type: Asset,
assets: [{
'id': 1,
'title': 'TITLE'
}],
});
store.getState().assets = a.assets
global.fetch = () => {return Promise.resolve('', 200)}
home = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Provider store={store}>
<Home />
</Provider>
</MuiThemeProvider>
);
});
it('renders the main page, including cards and appbar', () => {
...
});
afterEach(() => {
global.fetch = fetch;
});
});
使用全局上下文来存储组件可能很脆弱,对于任何大型项目来说可能都不是一个好主意。
相反,您可以使用依赖项注入 (DI) 模式,这是一种更正式的方法,用于根据您的运行时配置切换不同的组件依赖项(在本例中为获取)。 https://en.wikipedia.org/wiki/Dependency_injection
使用 DI 的一种简洁方法是使用控制反转 (IoC) 容器,例如: https://github.com/jaredhanson/electrolyte
我所做的是将 fetch
调用从组件移到存储库 class 中,然后从组件内部调用它。这样组件就独立于数据源,我可以将存储库切换为虚拟存储库,以便测试或更改实现,从执行获取到从 localStorage
、sessionStorage
获取数据IndexedDB、文件系统等
class Repository {
constructor() {
this.url = 'https://api.example.com/api/';
}
getUsers() {
return fetch(this.url + 'users/').then(this._handleResponse);
}
_handleResponse(response) {
const contentType = response.headers.get('Content-Type');
const isJSON = (contentType && contentType.includes('application/json')) || false;
if (response.ok && isJSON) {
return response.text();
}
if (response.status === 400 && isJSON) {
return response.text().then(x => Promise.reject(new ModelStateError(response.status, x)));
}
return Promise.reject(new Error(response.status));
}
}
class ModelStateError extends Error {
constructor(message, data) {
super(message);
this.name = 'ModelStateError';
this.data = data;
}
data() { return this.data; }
}
用法:
const repository = new Repository();
repository.getUsers().then(
x => console.log('success', x),
x => console.error('fail', x)
);
示例:
export class Welcome extends React.Component {
componentDidMount() {
const repository = new Repository();
repository.getUsers().then(
x => console.log('success', x),
x => console.error('fail', x)
);
}
render() {
return <h1>Hello!</h1>;
}
}