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();
}
});
};
Related
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!
Assuming I have a module which returns a promise.
I want to mock different outcomes of this promise to test the function where this module is part of. I mock the module like this:
jest.mock('../request', () => {
return () => new Promise((resolve, reject) => {
return resolve({
response: { ok: true }
});
});
});
My first test is running
test("The function resolves", () => {
const initialState = { apiData: getState("postData", {}, "ready", "POST") };
const store: any = mockStore(initialState);
return expect(
performApiRequest("postData/", {}, { data: "json" })(dispatch, () =>
store.getState()
)
).resolves.toBeUndefined();
});
The problem is now with testing an other function where the value that resolves is supposed to be different, for instance {response: { ok: false } }.
I already tried different things. First wrapping the mock in a function and give the response as an argument. --> fails for mocks can't take out of scope variables.
I tried to call jest.doMock within the test but this does not trigger the request correctly.
I tried to do
const mockResponse = jest.fn();
jest.mock("../request", () => {
return () =>
new Promise((resolve, reject) => {
return resolve({
mockResponse
});
});
});
And then call mockResponse.mockReturnValueOnce(value).
No success yet. Anybody sees a way out?
You can create a default mock function at the top level with jest.fn. Once you create the mock you can override the implementation of the function within the test case with mockImplementation or mockImplementationOnce. You can find more information about this in the Jest documentation.
import request from '../request';
jest.mock("../request", () =>
jest.fn(() =>
Promise.resolve({
response: {
ok: true
}
})
)
);
test("MyTest", () => {
request.mockImplementationOnce(() =>
Promise.resolve({
response: {
ok: false
}
})
);
});
answer with typescript would be:
import request from '../request';
jest.mock("../request", () =>
jest.fn(() =>
Promise.resolve({
response: {
ok: true
}
})
)
);
test("MyTest", () => {
(request as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
response: {
ok: true
}
})
);
});
Code
import { createUser } from '../services';
...
...
handleFormSubmit = () => {
this.setState({ loading: true });
createUser()
.then(() => {
this.setState({
loading: false,
});
})
.catch(e => {
this.setState({
error: e,
});
});
};
Test
it('rejects...', () => {
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
return wrapper.instance().handleFormSubmit()
.catch(e => {
console.log("State: ", wrapper.state());
expect(e).toEqual('error');
});
});
Mock
export const createUser = function() {
return new Promise((resolve, reject) => {
reject('error');
});
};
The test does force the code to go into the catch in the method. So the state does get set to 'error'.
But in my test, it doesn't do what I expect and wait for the Promise to reject before it tests for the state change.
I'm not sure what to try here, should I be using async/await?
So it's the createUser method I want to wait for but I'm not sure my implementation allows for this.
You should do something like this:
it('rejects...', () => {
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
return expect(wrapper.instance().handleFormSubmit()).rejects.toEqual('error');
});
I think it is cleaner this way. You can see this approach in the official docs.
It's important to note that .rejects (and .resolves) returns a promise, which is returned in the example above so that jest knows to wait on it. If you don't return it, you MUST await it:
it('rejects...', async () => {
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
await expect(wrapper.instance().handleFormSubmit()).rejects.toEqual('error');
});
The test fails because it's not aware that the subject is asynchronous. It can be fixed by using a done param or making the test function async.
Note it's also necessary to set the number of expected assertions so that the test will fail even if the catch branch is not taken.
async/await style:
it('rejects...', async () => {
expect.assertions(1);
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
await wrapper.instance().handleFormSubmit()
.catch(e => {
console.log("State: ", wrapper.state());
expect(e).toEqual('error');
});
});
Older style done param:
it('rejects...', done => {
expect.assertions(1);
const Container = createUserContainer(CreateUser);
const wrapper = shallow(<Container />);
wrapper.instance().handleFormSubmit()
.catch(e => {
console.log("State: ", wrapper.state());
expect(e).toEqual('error');
done();
});
});
Asynchronous Testing Reference
expect.assertions reference
Your code looks correct. Why do you say that it doesn't wait for the Promise to reject? The only difference I would make would be to make use of Jest's mocking capability, so change
Mock
export const createUser = function() {
return new Promise((resolve, reject) => {
reject('error');
});
};
to
Test
jest.mock('../services');
const services = require('../services');
const createUser = jest.spyOn(services, "createUser");
createUser.mockRejectedValue("error");
...
it('rejects...', () => {
There's no need to have a separate Mock file
In your code handleFormSubmit function should return Promise on which you can wait in your test. Also you need to return truthful data from success and error callback to resolve and reject the promise respectively.
handleFormSubmit = () => {
this.setState({ loading: true });
return createUser()
.then(() => {
this.setState({
loading: false,
});
return true;
})
.catch(e => {
this.setState({
error: e,
});
throw e;
});
};
Here in your actual code you have caught the error in catch handler and trying to catch it further in out test case code. Hence catch can not be chained further, while you can chain then multiple times.
For reference go through Promise documentations:
https://www.peterbe.com/plog/chainable-catches-in-a-promise
I need to test that when button is clicked and after promise resolve
state.message === 'loggedIn successfully'
class Login extends Component {
constructor() {
this.onLoginClick = this.onLoginClick.bind(this);
}
fetchLogin() {
return new Promise(function (resolve) {
reollve({ success: true });
})
}
onLoginClick() {
let that = this;
fetchLogin().then(function ({ success }) {
success ?
that.setState({ message: 'loggedIn successfully' }) :
that.setState({ message: 'Fail' });
})
}
render() {
return (<div>
<button onClick={this.onLoginClick}></button>
</div>)
}
}
I guess you are aware of jest simulate in order to simulate your click ( if not then simulate)
You should be able to use jest async/await or promises with jest, here is the link to the official doc
It should be something like this :
it('works with async/await and resolves', async () => {
const wrapper = mount(<Login />);
wrapper.find('button').simulate('click');
await expect(wrapper.state().message).resolves.toEqual('loggedIn successfully');
});
Since you are performing a test on a promise, you should wait for the function triggered by the button to execute before you can make the assertion (thus the await).
The code below should work for you case:
it('should change state on successful login', async () => {
const wrapper = mount(<Login />);
await wrapper.find('button').simulate('click');
expect(wrapper.state().message).toEqual("loggedIn successfully");
});
In my component I have ...
onSubmit = (e) => {
e.preventDefault();
const { history, versionStore } = this.props;
versionStore.add(this.state.formData)
.then(() => history.push('/'));
}
On my test...
it('after successfully submit should redirect to / page', () => {
const spy = jest.spyOn(minProps.history, 'push')
.mockImplementation((path) => {
console.log('called with ', path); // IS CALLED!
});
const wrapper = shallow(<Add.wrappedComponent {...minProps} />);
fetchMock.postOnce('/api/version', { name: 'v1' });
wrapper.setState({ formData: { name: 'v1' } });
wrapper.find('form').simulate('submit', { preventDefault: jest.fn() });
expect(spy).toHaveBeenCalledWith('/');
spy.mockReset();
spy.mockRestore();
});
The test fail with
called with /
expect(jest.fn()).toHaveBeenCalledWith(expected)
Expected mock function to have been called with: ["/"]
But it was not called.
your redirect is inside of asynchronous code and you are testing it in a synchronous manner, meaning when the test executes the promise is not resolved yet. I would tackle this in one of 2 ways
1 - test your submit function w/o the event, then you can return the promise and test the redirection after the promise chain is successful
2 - mock versionStore.add to be synchronous and immidattly execute it's then function.