I'm trying to track context through the async stack using node async_hooks. It works for most cases, however I have found this use case that I can't think how to resolve:
service.js:
const asyncHooks = require('async_hooks');
class Service {
constructor() {
this.store = {};
this.hooks = asyncHooks.createHook({
init: (asyncId, type, triggerAsyncId) => {
if (this.store[triggerAsyncId]) {
this.store[asyncId] = this.store[triggerAsyncId];
}
},
destroy: (asyncId) => {
delete this.store[asyncId];
},
});
this.enable();
}
async run(fn) {
this.store[asyncHooks.executionAsyncId()] = {};
await fn();
}
set(key, value) {
this.store[asyncHooks.executionAsyncId()][key] = value;
}
get(key) {
const state = this.store[asyncHooks.executionAsyncId()];
if (state) {
return state[key];
} else {
return null;
}
}
enable() {
this.hooks.enable();
}
disable() {
this.hooks.disable();
}
}
module.exports = Service;
service.spec.js
const assert = require('assert');
const Service = require('./service');
describe('Service', () => {
let service;
afterEach(() => {
service.disable();
});
it('can handle promises created out of the execution stack', async () => {
service = new Service();
const p = Promise.resolve();
await service.run(async () => {
service.set('foo');
await p.then(() => {
assert.strictEqual('foo', service.get());
});
});
});
});
This test case will fail because the triggerAsyncId of the promise created when calling next is the executionAsyncId of the Promise.resolve() call. Which was created outside the current async stack and is a separate context. I can't see any way to marry the next functions async context with the context it was created in.
https://github.com/domarmstrong/async_hook_then_example
I wrote a very similar package called node-request-context with a blog post to explain it.
You haven't define any value for foo and you are not asking for any value when calling service.get() without any key. But I guess that was a minor mistake when you wrote the question.
The main issue you named was the location of Promise.resolve. I agree, there is no way to make it work. This is exactly the reason you've create the run function, so you will catch the executionAsyncId and track your code using it. Otherwise, you couldn't track any context.
Your code was just for testing but if you really need, you can cheat by using arrow function:
it('can handle promises created out of the execution stack', async () => {
service = new Service();
const p = () => Promise.resolve();
await service.run(async () => {
service.set('foo', 'bar');
await p().then(() => {
assert.strictEqual('bar', service.get('foo'));
});
});
});
I found a solution, which is not perfect, but does work. Wrapping the original promise with Promise.all will resolve to the correct executionAsyncId. But it does rely on the calling code being aware of the promises context.
const assert = require('assert');
const Service = require('./service');
describe('Service', () => {
let service;
afterEach(() => {
service.disable();
});
it('can handle promises created out of the execution stack', async () => {
service = new Service();
const p = Promise.resolve();
await service.run(async () => {
service.set('foo');
await Promise.all([p]).then(() => {
assert.strictEqual('foo', service.get());
});
});
});
});
Related
Ima rookie using async/await but must now to use Redis-om. NN_walkd walks through a Redis database looking for loop-chains and does this by recursion. So the 2 questions I have is:
Am I calling the inner recursive NN_walkd calls correctly via async/await?
At runtime, the compSearchM proc is called first and seems to work (it gets 5 entries so it has to call NN_walkd 5 times). A NN_walkd is then recursively called, and then when it loops the 1st time it then calls compSearchK where the problems are. It seems to sit on the first Redis call in compSearchK (.search). Yet the code for compSearchK and compSearchM look basically identical.
main call
NN_walk = async function(req, db, cnode, pnode, chain, cb) {
var vegas, sneaker;
req.session.walk = [];
await NN_walkd(req, cnode, pnode, [], 1);
req.session.walk = null;
console.log('~~~~~~~~~~~~ Out of Walk ~~~~~~~~~~~~~~~');
cb();
};
redis.mjs
export class RedisDB {
constructor() {
...
this._companyRepo = ...
}
compSearchK(ckey) { // doesn't matter if I have a async or not here
return new Promise(async (resolve) => {
const sckey = await this._companyRepo.search()
.where('COMPANYKEY').equals(ckey)
.return.all();
if (sckey.length) {
const ttrr = await this._companyRepo.fetch(sckey[0].entityId);
resolve(ttrr.toJSON());
} else
resolve(null);
});
}
compSearchM(mkey) {
var tArr=[];
return new Promise(async (resolve) => {
const smkey = await this._companyRepo.search()
.where('MASTERKEY').equals(mkey)
.and('TBLNUM').equals(10)
.return.all();
if (smkey.length) {
for (var spot in smkey) {
const ttrr = await this._companyRepo.fetch(smkey[spot].entityId);
tArr.push(ttrr.toJSON());
}
resolve(tArr);
} else {
resolve(null);
}
});
}
walk.js
NN_walkd = async function(req, cnode, pnode, chain, lvl) {
...
if (cnode[1]) {
const sObj = await req.app.get('redis').compSearchK(cnode[1]);
if (sObj) {
int1 = (sObj.TBLNUM==1) ? null : sObj.CLIENTKEY;
(async () => await NN_walkd(req, [sObj.COMPANYKEY,int1], cnode, Array.from(chain), tlvl))()
}
} else {
const sArr = await req.app.get('redis').compSearchM(cnode[0]);
if (sArr.length) {
for (sneaker in sArr) {
(async () => await NN_walkd(req, [sArr[sneaker].COMPANYKEY,sArr[sneaker].CLIENTKEY], cnode, Array.from(chain), tlvl))()
}
} else {
console.log('no more links on this chain: ',cnode);
}
}
}
"doesn't matter if i have async or not here"
compSearchK(ckey) { // doesn't matter if I have a async or not here
return new Promise(async (resolve) => {
const sckey = await this._companyRepo.search()
.where('COMPANYKEY').equals(ckey)
.return.all();
if (sckey.length) {
const ttrr = await this._companyRepo.fetch(sckey[0].entityId);
resolve(ttrr.toJSON());
} else
resolve(null);
});
}
Of course it doesn't matter, because you're not using await inside of compSearchK!
You are using the explicit promise contructor anti-pattern. You should avoid it as it demonstrates lack of understanding. Here is compSearchK rewritten without the anti-pattern -
async compSearchK(ckey) {
// await key
const sckey =
await this._companyRepo.search()
.where('COMPANYKEY').equals(ckey)
.return.all();
// return null if key is not found
if (sckey.length == 0) return null;
// otherwise get ttrr
const ttrr = await this._companyRepo.fetch(sckey[0].entityId);
// return ttrr as json
return ttrr.toJSON();
}
I have a node script which goes on like
const { instance } = new SDK(id, authToken);
const data = await getAllModels(instance); // helper method which uses the sdk instance to return all models
items = await getItem(instance, id);
I have abstracted getAllModels and getItem into a helper module inside helper.js
exports.getAllModels = async (instance) => {
const { data } = await instance.getModels();
return data;
};
exports.getItem = async (instance, zuid) => {
const items = await instance.getItems(zuid);
return items;
};
I am trying to mock both the functions in my test so that I can expect the values based on my values.
jest.spyOn(helper, 'getAllModels').mockImplementation(() => {
console.log('Test');
return Promise.resolve('c');
});
console.log('Test');
jest.spyOn(helper, 'getItem').mockImplementation(() => {
console.log('Test 1');
return Promise.resolve('d');
});
const baseVal = await main(instance, token);
expect(baseVal).toBe("some value");
I can see that the mock values are not getting called and instead a direct call to the script is being used, what am I missing ?
From what I can see from your code, getAllModels and getItem are named exports from helper.js, which you can see from the use case you posted in your first code block.
So in your test file you could have something like the following:
const { getAllModels, getItem } = require('./helper');
jest.mock('./helper', () => {
return {
getAllModels: jest.fn(() => {
console.log('Test');
return Promise.resolve('c');
}),
getItem: jest.fn(() => {
console.log('Test 1');
return Promise.resolve('d');
}),
};
});
I think this is a cleaner implementation than using spyOn in this instance.
I'm using jest with nodejs and sequelize for my models. For my testing, I wanted to mock the returned value of findAll to cover test scenarios. Sorry if this is a very newbie question but I'm at dead-end on this one.
init-models.js
module.exports = function initModels(sequelize) {
//model relationship code here
...
...
//end of model relationship code
return {
records,
anotherModel,
alsoAnotherModel
};
};
repository.js
const sequelize = require('../sequelize');
const initModels = require('../model/init-models');
let {
records,
anotherModel,
alsoAnotherModel
} = initModels(sequelize);
const fetchRecords = async () => {
console.info('Fetching records...');
return await records.findAll({sequelize parameters here});
}
repository.test.js This will work but needs the flexibility to mock findAll() return value/or throw Error
const repository = require('../../../src/db/repository/repository');
const initModels = require('../../../src/db/model/init-models');
jest.mock('../../../src/db/model/init-models', () => {
return function() {
return {
records: {
findAll: jest.fn().mockImplementation(() => [1,2,3])
}
//the rest of the code for other models
}
}
});
describe('fetchRecords', () => {
beforeEach(()=> {
});
test('should return correct number of records', async () => {
const result = await repository.fetchRecords();
expect(result.size).toStrictEqual(3); //test passed
});
})
To allow mocking of results of findAll, I've tried extracting it so I can change the result per test scenario, but it was not working. What did I missed?
const mockRecordsFindAll = jest.fn();
jest.mock('../../../src/db/model/init-models', () => {
return function() {
return {
records: {
findAll: () => mockRecordsFindAll
}
//the rest of the code for other models
}
}
});
describe('fetchRecords', () => {
beforeEach(()=> {
mockRecordsFindAll.mockReset()
});
test('should return correct number of records', async () => {
mockRecordsFindAll.mockImplementation(() => [1,2,3]); //should expect length 3
const result = await repository.fetchRecords();
expect(result.size).toStrictEqual(3); //fails, findAll was not mocked
});
})
The issue is mockRecordsFindAll is being returned instead of executed.
As #Gid machined just returning mockRecordsFindAll causes initialization issues (due to hoisting).
The solution for this case is using the decorator pattern to allow mockRecordsFindAll to be initialized afterward.
const mockRecordsFindAll = jest.fn();
jest.mock('../../../src/db/model/init-models', () => {
return function() {
return {
records: {
findAll: function () {
return mockRecordsFindAll.call(this, arguments);
}
}
}
}
});
describe('fetchRecords', () => {
...
})
I'm getting below errors. Why I'm receiving response as undefined from the service?
Is there anything wrong I did for providing mock implementations?
Service:
export class SaveDataService{
async save() : Promise<any> {
try{
return this.someFunction()
} catch(ex){
throw new Error('some error occured')
}
}
async someFunction() : Promise<any>{
const response = {
"file" : "<htm><body>This is sample response</body></html>"
}
return Promise.resolve(response);
}
}
Test/Spec file:
import { SaveDataService } from "./save-data.service";
jest.mock('./save-data.service')
describe('tests for SaveDataService', () => {
it('when save method is called and success result is returned', async () => {
let mockSaveDataServiceSomeFunction = jest.fn().mockImplementation(() => {
return Promise.resolve('Success Result')
});
SaveDataService.prototype.someFunction = mockSaveDataServiceSomeFunction;
let spy = jest.spyOn(SaveDataService.prototype, 'someFunction');
let service = new SaveDataService();
let data = await service.save()
expect(data).toEqual('Success Result')
expect(spy).toHaveBeenCalled()
})
it('when save method is called and error is returned', async () => {
let mockSaveDataServiceSomeFunction = jest.fn().mockImplementation(() => {
throw new Error('ERROR')
});
SaveDataService.prototype.someFunction = mockSaveDataServiceSomeFunction;
let spy = jest.spyOn(SaveDataService.prototype, 'save');
let service = new SaveDataService();
service.save()
expect(spy).toThrowError('ERROR')
})
})
A mock replaces the dependency. You set expectations on calls to the dependent object, set the exact return values it should give you to perform the test you want, and/or what exceptions to throw so that you can test your exception handling code.
In this scenario, you are mocking save-data.service by calling jest.mock('./save-data.service'). So that your class may looks like this:
async save() : Promise<any> {
// do nothing or undefined
}
async someFunction() : Promise<any> {
// do nothing or undefined
}
So you must implement the body yourself to expect what exactly you want the method/function to do for you. You are mocking only the someFunction:
...
let mockSaveDataServiceSomeFunction = jest.fn().mockImplementation(() => {
return Promise.resolve('Success Result')
});
SaveDataService.prototype.someFunction = mockSaveDataServiceSomeFunction;
...
So when you call the save() method you still get nothing/undefined.
You are overwriting the whole behavior of the service that I think your test may not be useful. But you can fix your test this way:
import { SaveDataService } from "./save-data.service";
jest.mock('./save-data.service');
describe('tests for SaveDataService', () => {
beforeEach(() => {
SaveDataService.mockClear();
});
it('when save method is called and success result is returned', async () => {
const spy = jest
.spyOn(SaveDataService.prototype, 'save')
.mockImplementation(async () => Promise.resolve('Success Result'));
const service = new SaveDataService();
const data = await service.save();
expect(data).toEqual('Success Result');
expect(spy).toHaveBeenCalled();
})
it('when save method is called and error is returned', async () => {
const spy = jest
.spyOn(SaveDataService.prototype, 'save')
.mockImplementation(() => {
throw new Error('ERROR');
});
const service = new SaveDataService();
expect(service.save).toThrowError('ERROR');
expect(spy).toHaveBeenCalled();
});
});
I have used awilix to be able to have dependency injection in javascript to be able to have easier test. but now I want to mock a resolver that is set in my container for only a set of tests
In other words, I have a resolver that I want to mock it in my test for some reasons, (it is costly to call it so many times and it is a time consuming network call.) thus, I need to mock it in many of my tests for example in a test which is called called b.test.js, but I want it to call the actual function in a.test.js
here is my awilix config
var awilix = require('awilix');
var container = awilix.createContainer({
injectionMode: awilix.InjectionMode.PROXY,
});
var network = () => {
return new Promise((resolve, reject) => {
setTimeout(() => { resolve('data') }, 3000);
});
}
module.exports = container.register({ network: awilix.asValue(network) });
my test is
const container = require('../container');
container.register({
heavyTask: awilix.asValue(mockFunction),
});
describe('b', () => {
it('b', async () => {
const result = await container.resolve('network')();
});
});
somehow you've already done it
but don't config container like what you've done, because this way you're gonna have a single object of container, so if you change that object it'll be changed in all tests, instead do it this way
const awilix = require('awilix');
const heavyTask = () => new Promise((resolve, reject) => {
setTimeout(() => {
resolve('actual run');
}, 3000);
});
const configContainer = () => {
const container = awilix.createContainer({
injectionMode: awilix.InjectionMode.PROXY,
});
return container.register({
heavyTask: awilix.asValue(heavyTask),
});
}
module.exports = configContainer;
it seems you already know that you can overwrite your registrations, which might be the only vague part
so a.test.js can be written as
const { describe, it } = require('mocha');
const { expect } = require('chai');
const configContainer = require('../container');
const container = configContainer();
describe('a', () => {
it('a', async () => {
const res = await container.resolve('heavyTask')();
expect(res).to.eq('actual run');
});
});
and test b can be written as something like this
const awilix = require('awilix');
const { describe, it } = require('mocha');
const { expect } = require('chai');
const configContainer = require('../container');
const container = configContainer();
const heavyTask = () => 'mock run';
container.register({
heavyTask: awilix.asValue(heavyTask),
});
describe('b', () => {
it('b', async () => {
const res = await container.resolve('heavyTask')();
expect(res).to.eq('mock run');
});
});