绕过 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 中,然后从组件内部调用它。这样组件就独立于数据源,我可以将存储库切换为虚拟存储库,以便测试或更改实现,从执行获取到从 localStoragesessionStorage 获取数据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>;
  }
}