问题
I am attempting to write a test for a react component that sends out a fetch GET request after mounting. The test just needs to check if the component has rendered and when fetch runs I get this: ReferenceError: fetch is not defined
. I have searched around and cant seem to find anything that will fix my problem. I am using jest and Test Utils for testing the components.
My component code:
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;
My Test Code:
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', () => {}
It errors when trying to render Home into document.
I have tried fetch-mock but this only allows for mock calls for API testing, which I'm not trying to do and doesn't mock the fetch calls in my component.
Mocking Home wont work because its the component I am trying to test. Unless there's a way to mock the componentDidMount()
function that I have missed.
I just need a workaround for the fetch call. Any ideas??
EDIT: I'm using React's JSX for the component and JS for the test
回答1:
Try 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);
回答2:
Unfortunately I'm using babel, which is listed as a limitation for rewire, but I tried it anyways...
I Added:
...
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 />
...
And received the error:
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)
I'm assuming this is because babel:
rename[s] variables in order to emulate certain language features. Rewire will not work in these cases
(Pulled from the link chardy provided me)
However the path isn't a variable, so I wonder if babel is really renaming it, and the path is 100% correct for my component's location. I don't think it's because I'm using JSX because it just cant find the component, its not a compatibility issue... Rewire still may not work even if it finds the file though, unfortunately, but I'd like to give it a shot all the same.
回答3:
I found an answer that worked for me, and it was very simple without including any other dependencies. It was a simple as storing the main function into a variable and overwriting it, then restoring the proper function after the testcase
SOLUTION:
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;
});
});
回答4:
Using the global context to store components can be brittle and is probably not a good idea for any sizable project.
Instead, you can use the dependency injection (DI) pattern which is a more formalized method for switching out different component dependencies (in this case, fetch) based on your runtime configuration. https://en.wikipedia.org/wiki/Dependency_injection
One tidy way to employ DI is by using an Inversion of Control (IoC) container, such as: https://github.com/jaredhanson/electrolyte
回答5:
What I did is I moved the fetch
calls out of the components into a repository class then called that from within the component. That way the components are independent of the data source and I could just switch out the repository for a dummy repository in order to test or change the implementation from doing fetch to getting the data from localStorage
, sessionStorage
, IndexedDB, the file system, etc.
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; }
}
Usage:
const repository = new Repository();
repository.getUsers().then(
x => console.log('success', x),
x => console.error('fail', x)
);
Example:
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>;
}
}
来源:https://stackoverflow.com/questions/38317062/bypass-fetch-call-in-react-component