I am testing a saga generator function in my code.
Here is the code:
export function* mainFunction({ payload: { authorization } }) {
try {
const response = yield call(
fetch,
`${url}/api`,
{
headers: {
Authorization: authorization ,
},
}
);
if (response.status !== 200) {
throw new Error("Failed to load");
}
const jsonResponse = yield response.json();
const data = yield call(
helperFunction1,
Array.from(jsonResponse)
);
const updatedData = yield call(helperFunction2, data);
yield put(setData({ finalData: updatedData }));
yield put(helperFunction3({ authorization }));
} catch ({ message }) {
yield showError(message);
yield put(setError(message));
}
}
Here is the test:
const payload = {
authorization: "authorization",
};
const apiCall = call(
fetch,
`${url}/api`,
{
headers: {
Authorization: authorization,
},
}
);
describe("success", () => {
const saga = mainFunction({ payload });
const data = "data";
const response = {
status: 200,
json: () => data,
};
const jsonResponse = response.json();
it("should call API", () => {
expect(saga.next().value).toEqual(apiCall);
});
it("should get response with data", () => {
expect(saga.next(response).value).toEqual(data);
});
it("should call helperFunction1", () => {
expect(saga.next(jsonResponse).value).toEqual(
call(helperFunction1, Array.from(jsonResponse))
);
});
it("should call helperFunction2", () => {
expect(saga.next(data).value).toEqual(call(helperFunction2, data));
});
it("should dispatch success action with data", () => {
expect(saga.next(data).value).toEqual(
put(setData({ finalData: data }))
);
});
it("should dispatch status check", () => {
expect(saga.next(data).value).toEqual(
put(helperFunction3({ accessToken: "token" }))
);
});
it("should be done", () => {
expect(saga.next().done).toBe(true);
});
});
The first two tests run fine, however, the third test, to test the call of helperFunction1, each test from here is telling me that the actual yield is "fetch" and not the helperFunction1 that was expected. Basically, the "actual" result that it is yielding is the api variable declared before the test, the same expected result of the first test. The remaining tests pass, which appear to be written the same way as the third one. I am completely unsure of why the fetch function is yielded again for the third test while all the rest are correct. I thought the first saga.next() would have completed the fetch yield call. For the record, none of the helper functions themselves are generator functions.
The simple answer to this issue was that I was running the tests in isolation, therefore the yield order was not the expected order. When I ran all the tests related to this function at once, they all passed.
Related
I have two main functions. The first one gets the SOAP data from an API. The second one parses it to json and then formats it into the object I need to store. I have been logging and seeing the return values, and as of so far until now, it's been fine. However, I am calling exports.createReturnLabelfrom my test file and all I can see is Promise { <pending> } . I am returning a value at the last function. Does this code look weird to your? How can I clean it up?
const soapRequest = require('easy-soap-request');
const xmlParser = require('xml2json')
exports.createReturnLabel = async () => {
const xml = hidden
const url = 'https://ws.paketomat.at/service-1.0.4.php';
const sampleHeaders = {
'Content-Type': 'text/xml;charset=UTF-8',
};
const auth = async () => {
const {
response
} = await soapRequest({
url: url,
headers: sampleHeaders,
xml: xml,
timeout: 2000
});
const {
body,
statusCode
} = response;
return body
}
const fetchLabel = async () => {
const soapData = await auth();
try {
const json = xmlParser.toJson(soapData)
const labelData = JSON.parse(json)["SOAP-ENV:Envelope"]["SOAP-ENV:Body"]["ns1:getLabelResponse"]
return {
courier: 'dpd',
tracking_number: labelData["return"]["paknr"],
barCode: labelData["return"]["barcodecontent"],
printLabel: labelData["return"]["label"],
_ref: null
}
} catch (e) {
return (e)
}
}
return fetchLabel()
}
calling from my test file return console.log(file.createReturnLabel())
There's an async function call inside your code.
Should be: return await fetchLabel(), so that it awaits for completion before going on its merry way.
When running a coverage test, I am unable to get a specific block of code to be covered. It's a promise with a callback function (which sets the react state),
but I am having a hard time getting the callback function to be covered correctly in the coverage test. Here is the unit test:
TestPage.test.js
it('Handle form submit for entity with processToWaitFor', () => {
const wrapper = shallow( <TestPage data={{ processToWaitFor: 'submission' }} /> );
wrapper.instance().setState({ redirect: false });
wrapper.instance().submitForm();
const checkEntityFormStatus = jest.fn().mockImplementation(() => Promise.resolve('ended'));
const mockCallbackFunc = () => { wrapper.instance().setState({ redirect: true }) };
expect(checkEntityFormStatus('', '', '', mockCallbackFunc())).resolves.toBe('ended');
expect(wrapper.state().redirect).toEqual(true);
});
TestPage.js
submitForm = () => {
const {processToWaitFor} = this.props.data;
if (processToWaitFor) {
return checkEntityFormStatus( // Defined in APIcalls.js below
entity,
token,
processToWaitFor,
(response) => {
// Unit test not covering this callback which sets the state
this.setState({redirect: true});
return response;
}
);
}
}
APIcalls.js
export const checkEntityFormStatus = (
entity,
token,
processToWaitFor,
callback
) => {
const fetchStatus = async (n) => {
try {
const response = await API.checkEntityFormStatus(entity, token, processToWaitFor).then(API.handleResponse);
if (response == 'ended') {
return callback(response);
} else {
return new Error('No Status');
}
} catch (error) {
throw error;
}
};
};
Any idea how to get the unit test should look like so our coverage test covers the callback which sets the state when response is 'ended' from checkEntityFormStatus?
Hi guys I'm having trouble testing the below JS using Jest. It starts with waitForWorker. if the response is 'working' then it calls waitForWorker() again. I tried Jest testing but I don't know how to test an inner function call and I've been researching and failing.
const $ = require('jquery')
const axios = require('axios')
let workerComplete = () => {
window.location.reload()
}
async function checkWorkerStatus() {
const worker_id = $(".worker-waiter").data('worker-id')
const response = await axios.get(`/v1/workers/${worker_id}`)
return response.data
}
function waitForWorker() {
if (!$('.worker-waiter').length) {
return
}
checkWorkerStatus().then(data => {
// delay next action by 1 second e.g. calling api again
return new Promise(resolve => setTimeout(() => resolve(data), 1000));
}).then(worker_response => {
const working_statuses = ['queued', 'working']
if (worker_response && working_statuses.includes(worker_response.status)) {
waitForWorker()
} else {
workerComplete()
}
})
}
export {
waitForWorker,
checkWorkerStatus,
workerComplete
}
if (process.env.NODE_ENV !== 'test') $(waitForWorker)
Some of my test is below since i can't double check with anyone. I don't know if calling await Worker.checkWorkerStatus() twice in the tests is the best way since waitForWorker should call it again if the response data.status is 'working'
import axios from 'axios'
import * as Worker from 'worker_waiter'
jest.mock('axios')
beforeAll(() => {
Object.defineProperty(window, 'location', {
value: { reload: jest.fn() }
})
});
beforeEach(() => jest.resetAllMocks() )
afterEach(() => {
jest.restoreAllMocks();
});
describe('worker is complete after 2 API calls a', () => {
const worker_id = Math.random().toString(36).slice(-5) // random string
beforeEach(() => {
axios.get
.mockResolvedValueOnce({ data: { status: 'working' } })
.mockResolvedValueOnce({ data: { status: 'complete' } })
jest.spyOn(Worker, 'waitForWorker')
jest.spyOn(Worker, 'checkWorkerStatus')
document.body.innerHTML = `<div class="worker-waiter" data-worker-id="${worker_id}"></div>`
})
it('polls the correct endpoint twice a', async() => {
const endpoint = `/v1/workers/${worker_id}`
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint]])
expect(data).toMatchObject({"status": "working"})
})
await Worker.checkWorkerStatus().then((data) => {
expect(axios.get.mock.calls).toMatchObject([[endpoint],[endpoint]])
expect(data).toMatchObject({"status": "complete"})
})
})
it('polls the correct endpoint twice b', async() => {
jest.mock('waitForWorker', () => {
expect(Worker.checkWorkerStatus).toBeCalled()
})
expect(Worker.waitForWorker).toHaveBeenCalledTimes(2)
await Worker.waitForWorker()
})
I think there are a couple things you can do here.
Inject status handlers
You could make the waitForWorker dependencies and side effects more explicit by injecting them into the function this lets you fully black box the system under test and assert the proper injected effects are triggered. This is known as dependency injection.
function waitForWorker(onComplete, onBusy) {
// instead of calling waitForWorker call onBusy.
// instead of calling workerComplete call onComplete.
}
Now to test, you really just need to create mock functions.
const onComplete = jest.fn();
const onBusy = jest.fn();
And assert that those are being called in the way you expect. This function is also async so you need to make sure your jest test is aware of the completion. I notice you are using async in your test, but your current function doesnt return a pending promise so the test will complete synchronously.
Return a promise
You could just return a promise and test for its competition. Right now the promise you have is not exposed outside of waitForWorker.
async function waitForWorker() {
let result = { status: 'empty' };
if (!$('.worker-waiter').length) {
return result;
}
try {
const working_statuses = ['queued', 'working'];
const data = await checkWorkerStatus();
if (data && working_statuses.includes(data.status)) {
await waitForWorker();
} else {
result = { status: 'complete' };
}
} catch (e) {
result = { status: 'error' };
}
return result;
}
The above example converts your function to async for readability and removes side effects. I returned an async result with a status, this is usefull since there are many branches that waitForWorker can complete. This will tell you that given your axios setup that the promise will complete eventually with some status. You can then use coverage reports to make sure the branches you care about were executed without worrying about testing inner implementation details.
If you do want to test inner implementation details, you may want to incorporate some of the injection principals I mentioned above.
async function waitForWorker(request) {
// ...
try {
const working_statuses = ['queued', 'working'];
const data = await request();
} catch (e) {
// ...
}
// ...
}
You can then inject any function into this, even a mock and make sure its called the way you want without having to mock up axios. In your application you simply just inject checkWorkerStatus.
const result = await waitForWorker(checkWorkerStatus);
if (result.status === 'complete') {
workerComplete();
}
I am writing a script (see below) to make sure that an axios function throws an error when it receives a certain status code. I found out, however, that even when I make this test fail, Jest still says that the test passes, even though it returns an error in the console (see below). Why is Jest saying this test has passed when it actually failed? Does it have something to do with me trying to expect an error, so even if the test fails, jest still receives an error (that the test failed) and thinks this means that I got what I expected? Thanks.
foo.test.js:
import axios from 'axios';
jest.mock('axios', () => ({
get: jest.fn(() => Promise.resolve({ data: 'payload' })),
}));
const getData = async (url) => {
const response = await axios.get(url);
if (response.status !== 200) {
return response.text().then((error) => {
throw new Error(error.message);
});
} else {
return response.data;
}
};
test('testing that an error is thrown', async () => {
axios.get.mockImplementation(() =>
Promise.resolve({
data: {data: 'payload'},
status: 400,
text: () => Promise.resolve(JSON.stringify({message: 'This is an error.'})),
})
);
const expectedError = async () => {
await getData('sampleUrl');
};
// The error should return 'This is an error.' and instead
// is expecting 'foo', so this test should fail.
expect(expectedError()).rejects.toThrowError('foo');
});
You need two changes to get the test to fail as expected.
Don't stringify the resolved value from text
await the expect that uses rejects
Here is an updated version that fails as expected:
import axios from 'axios';
jest.mock('axios', () => ({
get: jest.fn(() => Promise.resolve({ data: 'payload' })),
}));
const getData = async (url) => {
const response = await axios.get(url);
if (response.status !== 200) {
return response.text().then((error) => {
throw new Error(error.message);
});
} else {
return response.data;
}
};
test('testing that an error is thrown', async () => {
axios.get.mockImplementation(() =>
Promise.resolve({
data: {data: 'payload'},
status: 400,
text: () => Promise.resolve({message: 'This is an error.'}), // <= don't stringify
})
);
const expectedError = async () => {
await getData('sampleUrl');
};
await expect(expectedError()).rejects.toThrowError('foo'); // <= await
});
I am trying to get response of redux-saga action which is returned from service. These are some snippets of my project.
// ACTION
const actions = {
LOAD_CURRENT_ACCOUNT: 'user/LOAD_CURRENT_ACCOUNT'
};
export const currentUser = () => ({
type: actions.LOAD_CURRENT_ACCOUNT
});
// SERVICE
export async function currentAccount() {
try {
const url = config.endpoints.user;
const res = await http.get(url);
return res.data;
} catch (err) {
if (err.response && err.response.data) {
notification.warning({
message: err.response.data.code,
description: err.response.data.message
});
}
return false;
}
}
// SAGA
export function* LOAD_CURRENT_ACCOUNT() {
const res = yield call(currentAccount);
if (res) {
yield put({
type: actions.SET_STATE,
payload: res.data
});
}
}
Right now, when I try to log the response it simply returns action type object i.e. "user/LOAD_CURRENT_ACCOUNT" instead of this output I'm expecting API response.
// abc.js
const res = await store.dispatch(currentUser());
console.log(res);
// RESULT: "user/LOAD_CURRENT_ACCOUNT"
How can I get the API response here?