My problem was that I am trying to make a unit test for a function but can't figure out how to test a part of it.
This is a react / redux action that does the following:
1) retrieves json data with an image url
2) loads the image into an Image instance and dispatches its size to the reducer (asynchronously when image is loaded using Image.onload)
3) dispatches that the fetch was completed to the reducer
The image onload happens asynchronously, so when I try to unit test it it wouldn't be called. Moreover, I can't just mock things out because the image instance is created within the function...
Here's the code I wanted to test (removing some checks, branching logic, and stuff):
export function fetchInsuranceCardPhoto() {
return dispatch => {
dispatch(requestingInsuranceCardPhoto());
return fetch(`${api}`,
{
headers: {},
credentials: 'same-origin',
method: 'GET',
})
.then(response => {
switch (response.status) {
case 200:
return response.json()
.then(json => {
dispatch(receivedInsuranceCardPhoto(json));
})
}
});
};
}
function receivedInsuranceCardPhoto(json) {
return dispatch => {
const insuranceCardFrontImg = json.insuranceCardData.url_front;
const insuranceCardBackImg = json.insuranceCardData.url_back;
if (insuranceCardFrontImg) {
dispatch(storeImageSize(insuranceCardFrontImg, 'insuranceCardFront'));
}
return dispatch(receivedInsuranceCardPhotoSuccess(json));
};
}
function receivedInsuranceCardPhotoSuccess(json) {
const insuranceCardFrontImg = json.insuranceCardData.url_front;
const insuranceCardBackImg = json.insuranceCardData.url_back;
const insuranceCardId = json.insuranceCardData.id;
return {
type: RECEIVED_INSURANCE_CARD_PHOTO,
insuranceCardFrontImg,
insuranceCardBackImg,
insuranceCardId,
};
}
function storeImageSize(imgSrc, side) {
return dispatch => {
const img = new Image();
img.src = imgSrc;
img.onload = () => {
return dispatch({
type: STORE_CARD_IMAGE_SIZE,
side,
width: img.naturalWidth,
height: img.naturalHeight,
});
};
};
}
Notice in that last storeImageSize
private function how there's an instance of Image created and an image.onload that is assigned to a function.
Now here's my test:
it('triggers RECEIVED_INSURANCE_CARD_PHOTO when 200 returned without data', async () => {
givenAPICallSucceedsWithData();
await store.dispatch(fetchInsuranceCardPhoto());
expectActionsToHaveBeenTriggered(
REQUESTING_INSURANCE_CARD_PHOTO,
RECEIVED_INSURANCE_CARD_PHOTO,
STORE_CARD_IMAGE_SIZE,
);
});
This test though will fail because the test finishes before the image.onload
callback is called.
How can I force the image.onload
callback to be called so that I can test that the `STORE_CARD_IMAGE_SIZE action gets broadcasted?
After some investigation, I found a very interesting javascript function that would solve my issue.
It is this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Here's how I used Object.defineProperty(...)
to solve my issue:
describe('fetchInsuranceCardPhoto', () => {
let imageOnload = null;
/** Override Image global to save onload setting here so that I can trigger it manually in my test */
function trackImageOnload() {
Object.defineProperty(Image.prototype, 'onload', {
get: function () {
return this._onload;
},
set: function (fn) {
imageOnload = fn;
this._onload = fn;
},
});
}
it('triggers RECEIVED_INSURANCE_CARD_PHOTO when 200 returned with data', async () => {
trackImageOnload();
givenAPICallSucceedsWithData();
await store.dispatch(fetchInsuranceCardPhoto());
imageOnload();
expectActionsToHaveBeenTriggered(
REQUESTING_INSURANCE_CARD_PHOTO,
RECEIVED_INSURANCE_CARD_PHOTO,
STORE_CARD_IMAGE_SIZE,
);
});
What I did here was use define property to override the setter of any instance of Image
. the setter would continue to get or set like normal but would also save the value (in this case a function) that was set to a variable in the scope of the unit test. After which, you can just run that function you captured before the verification step of your the test.
Gotchas
- configurable needs to be set
- note that defineProperty
is a different function than defineProperties
- This is bad practice in real code.
- remember to use the prototype
Hope this post can help a dev in need!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With