Testing a callback in Jest - javascript

I have a project I inherited and I am writing up a bunch of tests before I refactor it, and I am having trouble testing a callback.
static createNew(data) {
ApiActions.post(
'/api/projects',
data,
(err, response) => {
// this is the callback I want to test gets triggered
if (!err) {
this.hideCreateNew();
}
}
);
}
ApiActions just builds and executes the ajax request, the third param being the callback.
my test so far:
import ApiActions from '#helpers/api';
jest.mock('#helpers/api');
...
it('should createNew', () => {
const callback = jest.fn((cb) => cb());
Actions.createNew({ data: [] }, callback);
expect(ApiActions.post).toHaveBeenCalledWith(
'/api/projects',
{ data: [] },
expect.any(Function)
);
expect(callback).toHaveBeenCalled();
});

You could refactor your code such that it becomes more testable by factoring the callback in createNew into a separate function and use that as the default callback, but allow other callbacks to be passed in as well.
I believe it should be something like this (may be slightly wrong, but it should illustrate the idea):
static createCallback(err, response) {
if (!err) {
this.hideCreateNew();
}
}
static createNew(data, callback) {
// default to createCallback
callback = callback || createCallback;
ApiActions.post(
'/api/projects',
data,
callback
);
}
Now you can test both createNew and createCallback independently whether the behavior is as expected.

Related

Getting Undefined When Passing Function From An Object As Callback Within Another Function From Same Object

I'm not knowledgeable in JS and JS in Node RTE. But I tried writing simple functions as arrow functions in objects for later usage. One of these arrow functions (data.volatile.modularVariables.read) calls the readFile (asynchronous) function from FileSystem node native module, and passes the following into the parameters from the same object:
file path (data.persistent.paths.srcRoot)
encoding scheme (data.volatile.modularVariables.encoding.utf8)
call-back function (data.volatile.modularVariables.readCB) <-issue lays here
relevant Code (the object):
var data =
{
persistent:
{
states:
{
//exempt for question clarity
},
paths:
{
srcRoot: './vortex/root.txt'
}
},
volatile:
{
//much of the data exempt for question clarity
modularVariables:
{
encoding: {utf8:'utf8',hex:'hex',base64:'base64',BIN:(data,encoding)=>{Buffer.from(data,encoding)}},
readCB: (err,data)=>{if(err){console.log(`%c${data.volatile.debug.debugStrings.errCodes.read}: Error reading from file`,'color: red'); console.log(data);}},
writeCB: (err)=>{if(err){console.log(`%c${data.volatile.debug.debugStrings.errCodes.write}: Error writing to file`, 'color:red')}},
read: (file,encoding,cb)=>{fs.readFile(file,encoding,cb)}, //cb = readCB READ file srcRoot, pass into root(rootHash,encoding)
write: (file,data,cb)=>{fs.writeFile(file,data,cb)}, //cb = writeCB
checkInit: (symbol)=>{if(typeof symbol !== undefined){return symbol}}
},
debug:
{
functions:
{
append:
{
testProg:{program:{main:'system.node.upgrade'}}
}
},
debugStrings:
{
errCodes:
{
read: 'AFTERNET ERR 000',
write: 'AFTERNET ERR 001',
}
}
}
}
};
Aggregator Code:
testing()
{
data.volatile.modularVariables.read(data.persistent.paths.srcRoot,data.volatile.modularVariables.encoding.utf8,data.volatile.modularVariables.readCB(data));
};
Terminal Error:
readCB: (err,data)=>{if(err){console.log(`%c${data.volatile.debug.debugStrings.errCodes.read}: Error reading from file`,'color: red'); console.log(data);}},
^
TypeError: Cannot read property 'volatile' of undefined
Notes:
In aggregator code, I tried not passing "data" into callback
I tried passing "err" but says it's undefined
I tried not passing anything into CB
Conclusion:
Can someone point out what I'm doing wrong and why?
I couldn't tell from the comments if you've resolved the error, but I have a few suggestions that may help.
Aggregator Code
I noticed that you are sending the callback in that code and passing in the data object, which is assigned to the err argument and the data argument will be undefined:
testing() {
data.volatile.modularVariables.read(
data.persistent.paths.srcRoot,
data.volatile.modularVariables.encoding.utf8,
data.volatile.modularVariables.readCB(data)
);
}
When passing in the callback function, you only need to specify the callback function name as below. NodeJS will call the callback with the appropriate err and data arguments. I believe this should resolve your problem.
testing() {
data.volatile.modularVariables.read(
data.persistent.paths.srcRoot,
data.volatile.modularVariables.encoding.utf8,
data.volatile.modularVariables.readCB
);
}
Variable Naming
One thing that could help others read your code and spot issues without needing to run the code is making sure you don't have variables with the same name. I believe you are attempting to reference your data object outside of the callback function, but in the scope of the callback function, data will be primarily referenced as the argument passed in (see scope). When I ran your code, the debugger showed me that data was undefined before applying the fix above. After, it showed me that data was an empty string.
For this, you can either change your data object to be named something like myData or cypherData and it will not conflict with any of your other variables/parameters.
Full Solution
var cypherData = {
persistent: {
states: {
//exempt for question clarity
},
paths: {
srcRoot: "./vortex/root.txt"
}
},
volatile: {
//much of the data exempt for question clarity
modularVariables: {
encoding: {
utf8: "utf8",
hex: "hex",
base64: "base64",
BIN: (data, encoding) => {
Buffer.from(data, encoding);
}
},
readCB: (err, data) => {
console.log(data);
if (err) {
console.log(
`%c${data.volatile.debug.debugStrings.errCodes.read}: Error reading from file`,
"color: red"
);
}
},
writeCB: (err) => {
if (err) {
console.log(
`%c${data.volatile.debug.debugStrings.errCodes.write}: Error writing to file`,
"color:red"
);
}
},
read: (file, encoding, cb) => {
fs.readFile(file, encoding, cb);
}, //cb = readCB READ file srcRoot, pass into root(rootHash,encoding)
write: (file, data, cb) => {
fs.writeFile(file, data, cb);
}, //cb = writeCB
checkInit: (symbol) => {
if (typeof symbol !== undefined) {
return symbol;
}
}
},
debug: {
functions: {
append: {
testProg: { program: { main: "system.node.upgrade" } }
}
},
debugStrings: {
errCodes: {
read: "AFTERNET ERR 000",
write: "AFTERNET ERR 001"
}
}
}
}
};
cypherData.volatile.modularVariables.read(
cypherData.persistent.paths.srcRoot,
cypherData.volatile.modularVariables.encoding.utf8,
cypherData.volatile.modularVariables.readCB
);

JEST: Test case inside a function not getting values from the constructor

I tried writing test cases wrapped inside a class so that calling a method will execute the test case. But the url value will be initialized inside the beforeAll/ beforeEach block due to dependency factor. In that case i am not getting the url value, which results the failure of the test case execution.I could not pass url as an argument as well(url will only be initialized in beforeAll block). Is there any alternative soultion available to overcome this issue?
sampleTest.ts
export interface TestCaseArgumentsType {
baseUrl: string;
url: string;
}
export class Sample {
set args(value: TestCaseArgumentsType) {
this.arguments = value;
}
private arguments!: TestCaseArgumentsType;
sampleTestFunction() {
console.log(this.arguments.url); // **expected**: sampleUrl **actual**: cannot set property url of undefined
it('to check the before each execution effects in the test case', () => {
console.log(this.arguments.url); // sampleUrl
});
}
}
sampleTestSuite.test.ts
import { Sample, TestCaseArgumentsType } from './sampleTest';
describe('User Route', () => {
let sample = new Sample();
// Test suite
describe('GET Request', () => {
// Preparing Test Suite
beforeAll(async () => {
sample.args = <TestCaseArgumentsType>{ url: `sampleUrl` };
}, 20000);
// Executing
sample.sampleTestFunction();
});
});
The reason is the execution order of the code.
The key point is beforeAll function is executed before calling it, not before calling sample.sampleTestFunction(). So, when you call sample.sampleTestFunction() method, the statement sample.args = <TestCaseArgumentsType>{ url: 'sampleUrl' } inside the beforeAll function will not execute. The arguments property of sample is undefined. That's why you got error:
TypeError: Cannot read property 'url' of undefined
When jestjs test runner prepare to calling it, then, test runner will call beforeAll function firstly
sampleTest.ts:
export interface TestCaseArgumentsType {
baseUrl: string;
url: string;
}
export class Sample {
set args(value: TestCaseArgumentsType) {
this.arguments = value;
}
private arguments!: TestCaseArgumentsType;
sampleTestFunction() {
console.log('===execute 2===');
console.log(this.arguments.url); // another sampleUrl
it('to check the before each execution effects in the test case', () => {
console.log('===execute 4===');
console.log(this.arguments.url); // sampleUrl
});
}
}
sampleTestSuite.test.ts:
import { Sample, TestCaseArgumentsType } from './sampleTest';
describe('User Route', () => {
let sample = new Sample();
describe('GET Request', () => {
console.log('===execute 1===');
sample.args = <TestCaseArgumentsType>{ url: `another sampleUrl` };
beforeAll(async () => {
console.log('===execute 3===');
sample.args = <TestCaseArgumentsType>{ url: `sampleUrl` };
}, 20000);
sample.sampleTestFunction();
});
});
Unit test result:
PASS src/stackoverflow/58480169/sampleTestSuite.test.ts (7.092s)
User Route
GET Request
✓ to check the before each execution effects in the test case (3ms)
console.log src/stackoverflow/58480169/sampleTestSuite.test.ts:8
===execute 1===
console.log src/stackoverflow/58480169/sampleTest.ts:11
===execute 2===
console.log src/stackoverflow/58480169/sampleTest.ts:12
another sampleUrl
console.log src/stackoverflow/58480169/sampleTestSuite.test.ts:11
===execute 3===
console.log src/stackoverflow/58480169/sampleTest.ts:15
===execute 4===
console.log src/stackoverflow/58480169/sampleTest.ts:16
sampleUrl
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.89s
As you can see, I put some console.log to indicate the execution order of the code. The execution order is 1,2,3,4. Sorry for my English.

How do I test `image.onload` using jest in the context of redux actions (or other callbacks assigned in the action)

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!

How to store data from Pubnub history and make it available to all controllers?

I'm trying to get a history data from Pubnub.history(), store that data and update the views by using different controllers.
I've tried creating a service:
(function(){
'use strict';
angular.module('app')
.service('pubnubService', ['Pubnub',
pubnubService
]);
function pubnubService(Pubnub){
var history;
Pubnub.history({
channel : 'ParkFriend',
limit : 1,
callback : function(historyData) {
console.log("callback called");
history = historyData;
}
});
return {
getHistory : function() {
console.log("return from getHistory called");
return history;
}
};
}
})();
The problem is, getHistory() returns the data before Pubnub.history(). I need to make sure that history data is stored on history before returning it.
Since Pubnub.history is async, your getHistory function have to be an async function too.
Try the following:
function pubnubService(Pubnub) {
return {
getHistory: function(cb) { // cb is a callback function
Pubnub.history({
channel: 'ParkFriend',
limit: 1,
callback: function(historyData) {
console.log("callback called");
cb(historyData);
}
});
}
};
}
To use this service, you can't use it as a synchronous function (i.e., like var history = Pubnub.getHistory()), you need to pass a function as parameter to act like a callback.
Correct usage:
Pubnub.getHistory(function(history) { // here you have defined an anonym func as callback
console.log(history);
});

openSettings plugin cordova

I installed the plugin OpenSettings via node.js with this command in my project:
cordova plugin add https://github.com/erikhuisman/cordova-plugin-opensettings.git
But when I use method OpenSettings.setting() logcat return me an error:
OpenSettings.settings error at
file:///android_asset/www/plugins/nl.tapme.cordova.opensettings/www/OpenSettings.js:23
This is OpenSettings.js:
cordova.define("nl.tapme.cordova.opensettings.OpenSettings", function(require, exports, module) { module.exports = OpenSettings = {};
OpenSettings.settings = function(app, callback) {
cordova.exec(
// Success callback
callback,
// Failure callback
function(err) { console.log('OpenSettins.settings error'); },
// Native Class Name
"OpenSettings",
// Name of method in native class.
"settings",
// array of args to pass to method.
[]
);
};
OpenSettings.bluetooth = function (app, callback) {
cordova.exec(
// Success callback
callback,
// Failure callback
function(err) { console.log('OpenSettings.bluetooth error'); },
// Native Class Name
"OpenSettings",
// Name of method in native class.
"bluetooth",
// array of args to pass to method.
[]
);
};
OpenSettings.bluetoothStatus = function (app, callback) {
cordova.exec(
// Success callback
callback,
// Failure callback
function(err) { console.log('OpenSettins.bluetoothStatus error'); },
// Native Class Name
"OpenSettings",
// Name of method in native class.
"bluetoothStatus",
// array of args to pass to method.
[]
);
};
OpenSettings.bluetoothChange = function (callback) {
cordova.exec(
// Success callback
callback,
// Failure callback
function(err) { console.log('OpenSettins.bluetoothChange error'); },
// Native Class Name
"OpenSettings",
// Name of method in native class.
"bluetoothChange",
// array of args to pass to method.
[]
);
};
return OpenSettings;
});
Anyone can help me?
I would suggest you to test this plugin -> https://github.com/selahssea/Cordova-open-native-settings the first one you posted already did not work for me too.
Install it like this:
cordova plugin add https://github.com/selahssea/Cordova-open-native-settings.git
and use it like this:
cordova.plugins.settings.open(settingsSuccess,settingsFail);
Full snippet:
function settingsSuccess() {
console.log('settings opened');
}
function settingsFail() {
console.log('open settings failed');
}
function openSettingsNow() {
cordova.plugins.settings.open(settingsSuccess,settingsFail);
}
The plugin will open this overview:

Categories