I've got a saga that has some error handling logic in it - I want to test that a call is made three times and provide a response for each invocation.
The use case is that the saga retries on the first two errors before giving up, so I need a sequence of response: [fail, fail, success]
it("must succeed after the first two requests are failures", () =>
expectSaga(
sagaToTest
).provide([
[
call(getdata, request),
throwError(new Error("oops")) // do this twice and succeed on the third invication
]
])
.call(getdata, request)
.call(getdata, request)
.call(getdata, request)
.put(actions.itSucceeded("message"))
.run());
});
This is straightforward in other testing / mocking libraries but for some reason I can't seem to find the right documentation.
Thanks!
This library does exactly that https://www.npmjs.com/package/saga-test-stub
You'll need to split your code tho, first encapsulate the throwable call in a separate saga and test it
function* callApi(request: any){
try {
const response = call(getdata, request);
return {sucess:true,response}
}
catch (e){
return {sucess:false}
}
}
describe('callApi saga', () => {
let sagaStub: SagaStub;
beforeEach(() => {
sagaStub = stub(callApi, {});
});
describe('when call to api fails', () => {
beforeEach(() => {
jest.spyOn(api,'callApi').mockImplementation(()=> {
throw new Error()
});
it('should return success false', () => {
expect(saga).toBeDone({sucess:false})
});
});
});
describe('when call to api works', () => {
// ...
});
});
then stub the yielded values from the first saga
describe('sagaToTest', () => {
let sagaStub: SagaStub;
beforeEach(() => {
sagaStub = stub(sagaToTest, {});
when(sagaStub).yields(call(callApi,{})).doNext(
{succes: false},
{succes: false},
{succes: true, response: 'here you go'},
)
});
it('must succeed after the first two requests are failures', () => {
expect(sagaStub).toYield(
call(callApi,{}), //note: this is redundant since it is stubbed
call(callApi,{}), //note: this is redundant since it is stubbed
call(callApi,{}), //note: this is redundant since it is stubbed
put(actions.itSucceeded("message"))
)
});
});
Related
I've successfully mocked several routes in testing this application. My recent work involved making a GET request on the app's startup.
export const requestSettings = () =>
dispatch => {
console.log('Requesting settings');
const url = urls.SETTINGS_HTTP;
axios.get(url)
.then(res => {
console.log('response from axios');
return dispatch(updateSettingsFromBackend(res.data));
})
.catch((e) => {
console.log('axios error', e, e.config);
return dispatch(reportError('Request for initial settings failed.'));
});
};
In Cypress I mocked this route which allowed all my tests to run (since otherwise they'd fail on the failed HTTP request at the start of each test):
// commands.js
const urls = buildUrls(Cypress.env()); // we don't have the window yet
const settingsHttpUrl = urls.SETTINGS_HTTP;
const initialSettingsForTest = { ...defaultSettingsState, displayProgressIndicator: true };
const initialPayloadForTests = _createSettingsApiPayload(initialSettingsForTest);
Cypress.Commands.add("mockGetSettings", (code: number = 200) =>
cy.route({
method: 'GET',
url: settingsHttpUrl,
status: code,
response: code === 200 ? initialPayloadForTests : {},
}));
Cypress.Commands.add("mockPutSettings", (code: number) =>
cy.route({
method: 'PUT',
url: settingsHttpUrl,
status: code,
response: {},
}));
// the tests
describe('updating settings (requestParameterChange)', () => {
beforeEach(() => {
cy.mockGetSettings(200);
cy.visitSettings();
});
...
This worked fine.
My latest branch involves waiting to make this initial request until a certain websocket message is received. In my cypress tests I am able to dispatch that websocket handler to get the sequence going and ultimately call the same GET endpoint. With my back-end running, I am able to do this in the live app. The implementation is there.
However, in this branch, my mock is just not working! With the back-end up, the functionality works (which means it's hitting the real back-end, not the mock), and with the back-end down, it fails (because the mock isn't being hit).
describe('Settings Page', () => {
beforeEach(() => {
cy.server();
cy.clock();
});
describe('initial state', () => {
beforeEach(() => {
cy.mockGetSettings(200).as('getRequest');
cy.visitSettings();
});
it('starts in a waiting state, with no settings.', () => {
cy.contains('Waiting for settings...');
});
it.only('requests the settings when it receives a FIRMM status message', () => {
const message = { data: JSON.stringify({ status: 10 }) } as MessageEvent;
cy.dispatch(handleStatusMessage(message));
cy.wait('#getRequest');
});
(Note, by the way, the mockPutSettings command earlier in this post, which DOES WORK.)
cy.wait('#getRequest') fails with
CypressError: Timed out retrying: cy.wait() timed out waiting 5000ms for the 1st request to the route: 'getRequest'. No request ever occurred.
Chrome console shows: GET http://localhost:5000/settings/ net::ERR_EMPTY_RESPONSE
In fact, taking the app itself out of the equation also fails:
it('requests the settings when it receives a FIRMM status message', () => {
cy.visit(url);
cy.wait('#getRequest');
});
In the cypress window, we can see that the route is being created/set up, but it doesn't get hit.
What's happening?
PS. axios-mock-adapter DOES work for this:
function mockGet() {
// ... setup of constants as before ...
return new MockAdapter(axios).onGet(settingsHttpUrl).reply(() => {
console.log('MOCK AXIOS HIT');
return [200, initialPayloadForTests];
});
}
describe('Settings Page', () => {
beforeEach(() => {
cy.server();
cy.clock();
mock = mockGet();
});
afterEach(() => {
mock.restore();
});
The tests work, the response comes, MOCK AXIOS HIT prints to the console. However, mock.history.get is empty.
I'm trying to use jest to test my componentDidMount method:
componentDidMount() {
agent.Gatherings.getAll().then((result) => {
this.setState({ gatherings: result }) //no code coverage
}).catch((err) => {
this.setState({ gatherings: [] }) //no code coverage
})
}
yet one of my other tests works fine:
it('test gathering List is rendered', () => {
wrapper.setState({ gatherings: [TestGathering] })
expect(wrapper.find('MyList').length).toEqual(1);
});
I want to have every line covered in my testing. How can I get the lines in my componentDidMount() to all be tested in jest?
UPDATE, I'm importing a file directly into the test file. The file I'm importing is called agent.js. The code that gets called in the function whose lines are missed are:
agent.js
export const requests = {
get: url => fetch(url).then(res => res.json()),
post: (url, body) =>
fetch(url, {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json()) //also this line lacks coverage
}
export const Gatherings = {
getAll: () =>
requests.get(API_ROOT + '/gatherings')
}
export default {
Gatherings
}
Issue
A line of code has to run while a test is running to be included in the Jest code coverage.
Details
The two lines without coverage are the callbacks for the Promise returned by agent.Gatherings.getAll.
Promise callbacks get added to the PromiseJobs queue and run after the current message completes and before the next message runs.
This is why those lines are not currently included in the code coverage...right now they don't run until after your synchronous test completes.
Solution
You just need to make sure those two lines run while a test is running.
Details
The ideal approach is to await the Promise directly in your test.
In this case the Promise is not easily accessible from within the test so a different approach is needed.
Workaround
If agent.Gatherings.getAll is mocked to resolve or reject immediately then the Promise callback will be queued in PromiseJobs by the time the component finishes rendering.
To let the Promise callback run use an async test function and call await Promise.resolve(); which essentially queues the rest of the test at the end of PromiseJobs and lets any pending jobs run first:
import * as React from 'react';
import { shallow } from 'enzyme';
import { Comp } from './code'; // <= import your component here
import * as agent from './agent';
describe('Component', () => {
let spy;
beforeEach(() => {
spy = jest.spyOn(agent.Gatherings, 'getAll');
})
afterEach(() => {
spy.mockRestore();
})
it('updates when agent.Gatherings.getAll() resolves', async () => { // use an async test function
const response = [ 'gathering 1', 'gathering 2', 'gathering 3' ];
spy.mockResolvedValue(response);
const wrapper = shallow(<Comp />); // render your component
await Promise.resolve(); // let the callback queued in PromiseJobs run
expect(wrapper.state()).toEqual({ gatherings: response }); // SUCCESS
});
it('handles when agent.Gatherings.getAll() rejects', async () => { // use an async test function
spy.mockRejectedValue(new Error());
const wrapper = shallow(<Comp />); // render your component
await Promise.resolve(); // let the callback queued in PromiseJobs run
expect(wrapper.state()).toEqual({ gatherings: [] }); // SUCCESS
});
});
You should now have code coverage on the Promise callbacks in componentDidMount.
I'm using redux-saga to start multiple requests concurrently as described in the redux-saga docs. The all effect has an all or nothing semantics, similar to Promise.all.
Only if all effects succeed, yield all([...]) succeeds. However, I am doing several requests from which I expect some of them to fail and some of them to succeed. I would like to start all of them in parallel and consume the responses from those requests that have succeeded.
Therefore, I tried to wrap the request into a Promise that always resolves no matter whether the request was successful or not:
// watcher saga
export function* watchMultipleRequests() {
while(true) {
const {ids} = yield take('VIDEOS_REQUEST');
yield fork(doMultipleRequests, ids);
}
}
// worker saga
export function* doMultipleRequests(ids) {
const requests = ids.map(id => {
// api.buildVideoRequest returns a promise once it is invoked
const wrapper = ignoreErrors(api.buildVideoRequest, id);
return call(wrapper);
});
try {
const responses = yield all(requests);
yield put({type: 'VIDEOS_SUCCESS', responses});
} catch (error) {
// should not happen because we are always resolving the promise
console.log(error);
}
};
export function ignoreErrors(fn, ...args) {
return function* () {
yield new Promise(function (resolve) {
return fn(...args)
.then(response => {
console.log('success = ', response);
resolve(response);
})
.catch(response => {
console.log('error = ', response);
resolve(response);
});
});
}
}
I would like to handle the error cases in the reducer. However, if I fire n requests, the responses array contains n times undefined. Has anyone a clue on why this is not working?
The issue is that the ignoreErros function is a generator function.
Implementing it like this:
export function ignoreErrors(fn, ...args) {
return () => {
const ignoreErrorCallback = (response) => response;
return fn(...args).then(ignoreErrorCallback, ignoreErrorCallback);
};
}
is sufficient.
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
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.