Jest test of React component's setState update from within a Promise - reactjs

Given a React component called ListContainer with a function to load data as so:
loadList() {
this.setState({loading: true});
return this.props.api.get({
sort: this.props.sort.order,
search: this.props.search.query,
limit: this.props.pager.limit,
offset: this.props.pager.offset
}).then((response: ApiResponse) => {
this.setState({
listItems: response.data.records,
itemCount: response.data.meta.count,
error: undefined
})
}).catch((error: ApiError) => {
this.setState({
error: error
})
}).then(() => {
this.setState({
loading: false
})
})
}
I'm attempting to write a test that will ensure that the response data will be written to the component's state.
test('loads list items', () => {
const testApi = {
get: jest.fn(() => Promise.resolve())
};
// Omitting some additional props for brevity.
wrapper = shallow(<ListContainer api={testApi}/>);
const testItems = [
'Test Item 1',
'Test Item 2',
];
testApi.get.mockImplementationOnce(() =>
Promise.resolve({
data: {records: testItems}
})
);
return wrapper.instance().loadList()
.then(() => {
expect(wrapper.update().state().listItems).toBe(testItems)
});
})
I can see that my mock is returning the test data correctly (via console.log(response) in the loadList function) however the test is still failing. How do I ensure that setState has completed before asserting my expectations?

You're close!
You just need to add .data.meta.count to your response mock, right now the code ends up throwing an error since it isn't included in the current mock.
Just change your mock to this:
testApi.get.mockImplementationOnce(() =>
Promise.resolve({
data: {
records: testItems,
meta: { count: 2 } // <= include .data.meta.count
}
})
);
...and that should fix it!

Related

Why is a catch being triggered if my Fetch is succesful?

I am testing with component where on component did mount, it gets some data from an api,
I am mocking the response to be succesful, but still the catch block is being triggered, i think it shouldnt.
and i get this error:
TypeError: Network request failed
at XMLHttpRequest.xhr.onerror (C:\Users\jorge\Documents\testingreact\React-Testing-For-Beginners\node_modules\whatwg-fetch\fetch.js:436:16)
This is my component:
class MoviesList extends PureComponent {
state = {
movies: [],
};
async componentDidMount() {
try {
const res = await fetch(
'https://api.themoviedb.org/3/discover/movie?api_key=APIKEY&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1',
)
const movies = await res.json();
if (movies.success) {
this.setState({
movies: movies.results,
})
}
} catch (e) {
console.log(e);
}
}
render() {
...stuff
}
Then in my test i am mocking the result, and setting a property on the response called success to true
So, the catch error should not be triggered, however the console.log in the catch block is being logged.
Here is the test:
global.fetch = require('jest-fetch-mock')
afterEach(() => {
cleanup
})
const movies = {
success: true,
results: [
{
id: 'hi1',
title: 'title1',
poster_path: 'gfdsftg'
},
{
id: 'hi2',
title: 'title2',
poster_path: 'gfdsftg'
}
]
}
const movie = movies.results[0]
test('<MoviesList />', async () => {
fetch.mockResponseOnce(JSON.stringify(movies))
const {getByTestId, queryByTestId, getAllByTestId} = render(<MoviesList />)
...some assertions here that all pass
})
Why is the catch block being triggered?

Mocking a promise, and testing 'setState' in '.then()'

I'm writing unit tests for a React application. A click-handler calls a simple promise, and updates the state inside of '.then()'. I have successfully mocked the promise and am able to enter the correct if/else block using mock data returned from the resolved promise.
It seems that using a console.log to test the data shows that the data is partially incorrect. I'm also unable to test that the state has changed, as (I suppose) setState is asynchronous.
I have tried using .update(), .forceUpdate(), setTimeout(), and setImmediate(). I'm not sure what else to try to be able to test that the state has changed correctly.
Here is the method being tested:
this.handleClick = () => {
sendMessage(this.urlParams.data1, this.urlParams.data2, this.urlParams.data3)
.then((data) => {
if (data.messageType === 'error') {
console.log(data.message);
this.setState({
error: data.message,
}, () => {
console.log(data);
});
} else {
this.doSomething();
}
});
};
Here is the mock of the 'sendMessage' method, which is working as expected:
export const sendPnrgovMessage = () => Promise.resolve({ data: { messageType: 'error', message: 'asdf' } });
Here is the test itself:
it('should send the message when the button is clicked', () => {
renderedInstance = rendered.instance();
renderedInstance.handleClick();
renderedInstance.forceUpdate(() => {
expect(rendered.state('error')).toEqual('asdf');
});
});
I've also tried without the '.forceUpdate()', the result is the same.
There are two issues with the result.
1.) 'data' in the '.then()' evaluates to this:
{ messageType: 'error', message: undefined }
Why would message be undefined, while messageType is correct?
2.) The expected value of this.state.error is 'asdf', or in the above case, 'undefined', but it is actually 'null', which is the initialized value. I assumed that this is because setState is asynchronous and is not being updated by the time the test has finished. I have not found a way around this.
Have you tried mocking the function implementation using jest?
import { sendMessage } from './file';
describe('should test component', () => {
beforeAll(() => {
sendMessage = jest.fn().mockResolvedValue({ data: { message, Type: 'error', message: 'asdf' } });
});
afterAll(() => sendMessage.mockRestore());
it('should send the message when the button is clicked', () => {
renderedInstance = rendered.instance();
renderedInstance.handleClick();
renderedInstance.forceUpdate(() => {
expect(rendered.state('error')).toEqual('asdf');
});
});
});
You made 2 mistakes in your code.
The return value of sendMessage is { data: { messageType: 'error', message: 'asdf' } }
so you should receive the message as {data}. curly brack is required.
When we use the arrow function, we must avoid the 'this' context because it doesn't bind own this.
The answer is as follows:
this.handleClick = () => {
const ref = this;
sendMessage(this.urlParams.data1, this.urlParams.data2, this.urlParams.data3)
.then(({data}) => {
if (data.messageType === 'error') {
console.log(data.message);
ref.setState({
error: data.message,
}, () => {
console.log(data);
});
} else {
ref .doSomething();
}
});
};

How to wait in enzyme for a promise from a private function?

I'm a novice at react and any javascript testing frameworks.
I have a simple component that retrieves an item from the API and shows them to the screen.
The function getItems() is called from componentWillMount.
Is it possible to wait until getItems() has finished before making my assertions?
ItemDetails.js
class ItemDetails extends Component {
constructor(props) {
super(props);
this.state = {
details: ''
}
}
componentWillMount() {
this.getItem();
}
getItem() {
const itemId = this.props.match.params.id;
fetch(`/api/items/${itemId}`)
.then(res => res.json())
.then(details => this.setState({ details }));
}
render() {
const details = this.state.details;
return (
<div>
<h1>{details.title}</h1>
...
</div>
);
}
}
export default ItemDetails;
ItemDetails.test.js
describe('ItemDetails', () => {
it('should render a div with title', () => {
const details = {
_id: 1,
title: 'ItemName'
};
fetch.mockResponseOnce(JSON.stringify(details));
const wrapper = mount(<ItemDetails match={{ params: {id: 1} }} />);
expect(wrapper.find('div').find('h1').text()).toBe('ItemName');
});
});
The answer above works, but it invites to test implementation details:
as a rule of thumb, accessing the instance of a wrapper (.instance()) and calling update() are smells of tests that know too much about the way the code under test works in my opinion, it is not testing a behavior, it is testing an implementation
having the method return a promise and wait on that promise before executing the expectation breaks the privacy of the method: it make the code hard to refactor
What you really want is to wait on all promises to be resolved, without accessing a handle to those promises (that would break privacy). Try this
const wait = () => new Promise(resolve => setTimeout(resolve));
it('does something', () => {
renderFnThatCallsAPromiseInternally();
return wait().then(() => {
expect(somethingDependentOnPromiseExecution).to.be.present();
});
});
This will wait for all inner promises to resolve, provided wait is called after the code that enqueues promises (rendering the component) is called. The reason lies in how the JS event loop works, it's finicky, but very well explained by Jake Archibald in this talk: https://www.youtube.com/watch?v=cCOL7MC4Pl0.
Hope that helps.
Could you try:
describe('ItemDetails', () => {
it('should render a div with title', () => {
const details = {
_id: 1,
title: 'ItemName'
};
fetch.mockResponseOnce(JSON.stringify(details));
const wrapper = shallow(<ItemDetails match={{ params: {id: 1} }} />);
// manually call function
wrapper.instance().getItem();
// update to re-render component
wrapper.update();
expect(wrapper.find('div').find('h1').text()).toBe('ItemName');
});
});
If it doesn't help I think you will need to return Promise from your function (base on this example):
getItem() {
const itemId = this.props.match.params.id;
return fetch(`/api/items/${itemId}`)
.then(res => res.json())
.then(details => this.setState({ details }));
}
describe('ItemDetails', () => {
it('should render a div with title', () => {
const details = {
_id: 1,
title: 'ItemName'
};
fetch.mockResponse(JSON.stringify(details)); //response gets called twice
const wrapper = mount(<ItemDetails match={{ params: {id: 1} }} />);
// return Promise so Jest will wait until it's finished
return wrapper.instance().getItem().then(() => {
wrapper.update();
}).then(() => {
expect(wrapper.find('div').find('h1').text()).toBe('ItemName');
})
});
});

How to unit test Promise catch() method behavior with async/await in Jest?

Say I have this simple React component:
class Greeting extends React.Component {
constructor() {
fetch("https://api.domain.com/getName")
.then((response) => {
return response.text();
})
.then((name) => {
this.setState({
name: name
});
})
.catch(() => {
this.setState({
name: "<unknown>"
});
});
}
render() {
return <h1>Hello, {this.state.name}</h1>;
}
}
Given the answers below and bit more of research on the subject, I've come up with this final solution to test the resolve() case:
test.only("greeting name is 'John Doe'", async () => {
const fetchPromise = Promise.resolve({
text: () => Promise.resolve("John Doe")
});
global.fetch = () => fetchPromise;
const app = await shallow(<Application />);
expect(app.state("name")).toEqual("John Doe");
});
Which is working fine. My problem is now testing the catch() case. The following didn't work as I expected it to work:
test.only("greeting name is 'John Doe'", async () => {
const fetchPromise = Promise.reject(undefined);
global.fetch = () => fetchPromise;
const app = await shallow(<Application />);
expect(app.state("name")).toEqual("<unknown>");
});
The assertion fails, name is empty:
expect(received).toEqual(expected)
Expected value to equal:
"<unknown>"
Received:
""
at tests/components/Application.spec.tsx:51:53
at process._tickCallback (internal/process/next_tick.js:103:7)
What am I missing?
The line
const app = await shallow(<Application />);
is not correct in both tests. This would imply that shallow is returning a promise, which it does not. Thus, you are not really waiting for the promise chain in your constructor to resolve as you desire. First, move the fetch request to componentDidMount, where the React docs recommend triggering network requests, like so:
import React from 'react'
class Greeting extends React.Component {
constructor() {
super()
this.state = {
name: '',
}
}
componentDidMount() {
return fetch('https://api.domain.com/getName')
.then((response) => {
return response.text()
})
.then((name) => {
this.setState({
name,
})
})
.catch(() => {
this.setState({
name: '<unknown>',
})
})
}
render() {
return <h1>Hello, {this.state.name}</h1>
}
}
export default Greeting
Now we can test it by calling componentDidMount directly. Since ComponentDidMount is returning the promise, await will wait for the promise chain to resolve.
import Greeting from '../greeting'
import React from 'react'
import { shallow } from 'enzyme'
test("greeting name is 'John Doe'", async () => {
const fetchPromise = Promise.resolve({
text: () => Promise.resolve('John Doe'),
})
global.fetch = () => fetchPromise
const app = shallow(<Greeting />)
await app.instance().componentDidMount()
expect(app.state('name')).toEqual('John Doe')
})
test("greeting name is '<unknown>'", async () => {
const fetchPromise = Promise.reject(undefined)
global.fetch = () => fetchPromise
const app = shallow(<Greeting />)
await app.instance().componentDidMount()
expect(app.state('name')).toEqual('<unknown>')
})
By the looks of this snippet
.then((response) => {
return response.text();
})
.then((name) => {
this.setState({
name: name
});
})
it seems that text would return a string, which then would appear as the name argument on the next 'then' block. Or does it return a promise itself?
Have you looked into jest's spyOn feature? That would help you to mock not only the fetch part but also assert that the setState method was called the correct amount of times and with the expected values.
Finally, I think React discourages making side effects inside constructor. The constructor should be used to set initial state and other variables perhaps. componentWillMount should be the way to go :)
Recently, I have faced the same issue and ended up resolving it by following way
(taking your code as an example)
test.only("greeting name is 'John Doe'", async () => {
const fetchPromise = Promise.resolve(undefined);
jest.spyOn(global, 'fetch').mockRejectedValueOnce(fetchPromise)
const app = await shallow(<Application />);
await fetchPromise;
expect(app.state("name")).toEqual("<unknown>");});
Another way if you don't want to call done then return the next promise state to jest. Based on result of assertion( expect ) test case will fail or pass.
e.g
describe("Greeting", () => {
test("greeting name is unknown", () => {
global.fetch = () => {
return new Promise((resolve, reject) => {
process.nextTick(() => reject());
});
};
let app = shallow(<Application />);
return global.fetch.catch(() => {
console.log(app.state());
expect(app.state('name')).toBe('<unknown>');
})
});
});

Redux-observable: failed jest test for epic

I followed the steps from documentation to test epic.
...
store.dispatch({ type: FETCH_USER });
expect(store.getActions()).toEqual([
{ type: FETCH_USER },
{ type: FETCH_USER_FULFILLED, payload }
]);
...
But I get failed because second action is been received some later like following.
Test failed
Expected value to equal:
[{"type": "FETCH_USER"}, {"type": "FETCH_USER_FULFILLED", "payload": [some]}]
Received:
[{"type": "FETCH_USER"}]
Difference:
- Expected
+ Received
## -1,20 +1,5 ##
Array [
Object {"type": "FETCH_USER"},
Object {"type": "FETCH_USER_FULFILLED", "payload": [some]} ] // this is what should be.
So I think I should know when the dispatch is finished or some like that.
How can I solve this?
I used fetch() and Rx.Observable.fromPromise instead of ajax.getJSON()
Here is my epic.
const fetchUserEpic = (action$) =>
action$
.ofType(FETCH_USER)
.mergeMap(() => {
return Rx.Observable.fromPromise(api.fetchUser())
.map((users) => ({
type: FETCH_USER_FULFILLED,
payload: { users }
}))
.catch((error) => Rx.Observable.of({
type: FETCH_USER_ERROR,
payload: { error }
}))
.takeUntil(action$.ofType(FETCH_USER_CANCELLED))
})
The reason is that promises always resolve on the next microtask so your api.fetchUser() isn't emitting synchronously.
You'll need to either mock it out, use something like Promise.resolve().then(() => expect(store.getActions).toEqual(...) to wait until the next microtask, or you can experiment with testing your epics directly without using redux.
it('Epics with the appropriate input and output of actions', (done) => {
const action$ = ActionsObservable.of({ type: 'SOMETHING' });
somethingEpic(action$, store)
.toArray() // collects everything in an array until our epic completes
.subscribe(actions => {
expect(actions).to.deep.equal([
{ type: 'SOMETHING_FULFILLED' }// whatever actions
]);
done();
});
});
This will be our preferred testing story in the docs when I (or someone else) has time to write them up. So instead of using redux and the middleware in your tests, we just call the epic function directly with our own mocks. Much easier and cleaner.
With that approach, we can leverage the new dependency injection feature of redux-observable: https://redux-observable.js.org/docs/recipes/InjectingDependenciesIntoEpics.html
import { createEpicMiddleware, combineEpics } from 'redux-observable';
import { ajax } from 'rxjs/observable/dom/ajax';
import rootEpic from './somewhere';
const epicMiddleware = createEpicMiddleware(rootEpic, {
dependencies: { getJSON: ajax.getJSON }
});
// Notice the third argument is our injected dependencies!
const fetchUserEpic = (action$, store, { getJSON }) =>
action$.ofType('FETCH_USER')
.mergeMap(() =>
getJSON(`/api/users/${payload}`)
.map(response => ({
type: 'FETCH_USER_FULFILLED',
payload: response
}))
);
import { ActionsObservable } from 'redux-observable';
import { fetchUserEpic } from './somewhere/fetchUserEpic';
const mockResponse = { name: 'Bilbo Baggins' };
const action$ = ActionsObservable.of({ type: 'FETCH_USERS_REQUESTED' });
const store = null; // not needed for this epic
const dependencies = {
getJSON: url => Observable.of(mockResponse)
};
// Adapt this example to your test framework and specific use cases
fetchUserEpic(action$, store, dependencies)
.toArray() // buffers all emitted actions until your Epic naturally completes()
.subscribe(actions => {
assertDeepEqual(actions, [{
type: 'FETCH_USER_FULFILLED',
payload: mockResponse
}]);
});
First, use isomorphic-fetch instead of Observable.ajax for nock support, like this
const fetchSomeData = (api: string, params: FetchDataParams) => {
const request = fetch(`${api}?${stringify(params)}`)
.then(res => res.json());
return Observable.from(request);
};
So my epic is:
const fetchDataEpic: Epic<GateAction, ImGateState> = action$ =>
action$
.ofType(FETCH_MODEL)
.mergeMap((action: FetchModel) =>
fetchDynamicData(action.url, action.params)
.map((payload: FetchedData) => fetchModelSucc(payload.data))
.catch(error => Observable.of(
fetchModelFail(error)
)));
Then, you may need an interval to decide when to finish the test.
describe("epics", () => {
let store: MockStore<{}>;
beforeEach(() => {
store = mockStore();
});
afterEach(() => {
nock.cleanAll();
epicMiddleware.replaceEpic(epic);
});
it("fetch data model succ", () => {
const payload = {
code: 0,
data: someData,
header: {},
msg: "ok"
};
const params = {
data1: 100,
data2: "4"
};
const mock = nock("https://test.com")
.get("/test")
.query(params)
.reply(200, payload);
const go = new Promise((resolve) => {
store.dispatch({
type: FETCH_MODEL,
url: "https://test.com/test",
params
});
let interval: number;
interval = window.setInterval(() => {
if (mock.isDone()) {
clearInterval(interval);
resolve(store.getActions());
}
}, 20);
});
return expect(go).resolves.toEqual([
{
type: FETCH_MODEL,
url: "https://test.com/assignment",
params
},
{
type: FETCH_MODEL_SUCC,
data: somData
}
]);
});
});
enjoy it :)

Resources