I have the following scenario:
file1.js:
async function fctionA(event) {
console.log('In fctionA()!');
let responseA = null;
const formattedEvent = formatEvent(event);
...
const b = await fctionB(formattedEvent);
responseA = someLogicUsing(b);
return responseA; // responseA will depend on 'b'
}
file2.js:
async function fctionB(formattedEvent) {
console.log('Now in fctionB()!');
let resB = null;
...
const c = await fctionC(formattedEvent);
...
resB = someLogicDependingOn(c); // resB will depend on 'c'
return resB;
}
async function fctionC(formattedEvent) {
console.log('AND now in fctionC()!');
let c = await someHttpRequest(formattedEvent);
...
return c;
}
Side notes:
Don't mind formatEvent(), someLogicUsing() orsomeLogicDependingOn() too much. Assume it's just sync logic using provided data)
formattedEvent would be anything depending on the original input event. Just to note functions will use it.
PROBLEM:
What i want to do is to unit test fctionA(), using Jasmine: I want to check the responseA after appying the logic on fctionB(), but mock the result of fctionC().
My (clearly) naive approach was:
file1.spec.js
import * as Handler from '../src/file1';
import * as utils from '..src//file2';
describe('fctionA', () => {
let response = null;
beforeAll(async () => {
const mockedEventA = { mockedInput: 'a' };
const mockedC = { mockedData: 1 };
const expectedResponse = { mockedResponse: 1234 };
spyOn(utils, 'fctionB').and.callThrough());
spyOn(utils, 'fctionC').and.returnValue(Promise.resolve(mockedC));
response = await Handler.fctionA(mockedEventA);
});
it('should return a proper response', () = {
expect(response).toEqual(expectedResponse);
});
});
But after checking logs, i can see that ´fctionC()´ does get executed (when as far as i understood, it shouldn't), therefore does not return the mocked result.
Then, after some try and error, i invoked fctionC() directly in fctionA() (instead of indirectly invoking it through´fctionB()´) just to see what happens, and I can spy it and return a mocked value. fctionC() does not execute (can't see log).
So, that makes me think that, at least the way I'm trying to spy on functions, only work for functions that are directly invoked by the function I'm calling, but not for nested ones-
I'm clearly not an expert in Jasmine, so I can't figure out another option. I looked a lot into docs and posts and blogs but nothing worked for me.
Is there a way to achieve what I try here? I guess I might be doing something really silly or not thinking it through.
Thanks!
Related
I have a function, MyComposedFunction, which is the function composition of 3 functions the second function, fn2, performs a POST request (a side effect) using the result of fn1 and passes this value to fn3.
const fn2 = async (fn1Result) => {
const result = await fetch(fn1Result.url, fn1Result.payload);
// some business logic
return fn2Results;
};
const MyComposedFunction = compose(fn3, fn2, fn1);
// My Test
expect(MyComposedFunction('hello world')).toBe('expected result');
I'd like to avoid writing unit tests for fn3, fn2, and fn1 and instead only test MyComposedFunction. My rationale is that it should not matter whether MyComposedFunction uses compose(...) or is one long giant function as long as MyComposedFunction works.
Is it possible to write a test for MyComposedFunction without having to mock fn2?
I would think that this must be a relatively common situation when trying to do functional programming in JavaScript but haven't been able to find a helpful resource so far.
You can dependency inject fetch into your function like so
const fn2Thunk = (fetcher = fetch) => async (fn1Result) => {
const result = await fetcher(fn1Result.url, fn1Result.payload);
// some business logic
return fn2Results;
};
const fetchMock = async () => ({ result: "blah" })
const MyComposedFunctionTest = compose(fn3, fn2Thunk(fetchMock), fn1);
// My Test
const result = await MyComposedFunctionTest('hello world')
expect(result).toBe('expected result');
Looks nobody on internet describes similar problem, and similar solution is not working for me.
I am trying to scrape webpage so I created class for parser and one of the method looks like follows:
get chListUrl() {
return "http://www.teleman.pl/program-tv/stacje";
}
getChannels() {
var dict = {};
axios.get(this.chListUrl).then(function (response) {
var $ = cheerio.load(response.data);
var ile_jest_stacji = $('#stations-index a').length;
$('#stations-index a').each( (i,elem) => {
let href = $(elem).attr('href');
let kodstacji = href.replace(/\/program-tv\/stacje\//ig,'');
let nazwastacji = $(elem).text();
dict[nazwastacji]=kodstacji;
});
return dict;
}).catch(function (error) {
console.log(error);
return null;
}).finally(function() {
console.log("Koniec");
});
}
And problem is getChannels must be indirectly asynchronous because it contains axios BUT
let tm = new TM();
var a = tm.getChannels();
a is always undefined and it should be dictionary! Such construct means "assing to variable a result of execution of tm.getChannels()" so assignment should always be done AFTER whole function ends. Otherwise such syntax in language is useless because you will never be sure what value is stored in variable, and such errors are difficult to find and debug.
var a = await tm.getChannels();
NOT WORKING -> SyntaxError: await is only valid in async function (huh?)
adding async to getChannels() changes nothing.
Assing async to getChannels() and remove 'await' from assignment returns Promise{undefined} (huh?)
putting async before axios changes nothing as response is already handled by .then()
changing return dict to return await dict gives another "await is only valid in async function" (huh? axios is asynchronous)
I'm scratching my head over this for 2 weeks.
In Swift when something is return in completion handler it is assigned to variable in proper moment, why something returned by Promise not works the same way?
You need to be inside an async function to use the await expression:
The await operator is used to wait for a Promise. It can only be used inside an async function.
await operator on MDN
Example sourced from MDN:
async function f1() {
var x = await resolveAfter2Seconds(10);
console.log(x); // 10
}
f1();
Fixing your issue
class TM {
get chListUrl() {
return "http://www.teleman.pl/program-tv/stacje";
}
async getChannels() { // you need not use get syntax
let dict = {};
try { // we will be handling possible errors with try catch instead of reject
const response = await axios.get(this.chListUrl);
let $ = cheerio.load(response.data);
let ile_jest_stacji = $('#stations-index a').length;
$('#stations-index a').each( (i,elem) => {
let href = $(elem).attr('href');
let kodstacji = href.replace(/\/program-tv\/stacje\//ig,'');
let nazwastacji = $(elem).text();
dict[nazwastacji]=kodstacji;
});
return dict;
} catch(ex) {
console.log(ex);
return null
}
}
}
// let's make it work!
(async function() {
const tm = new TM()
const channels = await tm.getChannels()
// do whatever you want with channels
console.log(channels)
})()
Now, you're probably not going to call getChannels out of nowhere like this instead you will probably be inside a function that you yourself defined, you need to add the async keyword to this function. Whatever block function your code is in needs to be async.
If you want to use the async/await syntax you should remove the .then() syntax and you can resolve that way:
async getChannels() {
const response = await axios.get(this.chListUrl);
return response
}
You can learn more about async/await in the link: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
I have a function in my project which calls a function from bootstrapper object of window. Below is the function:
export default function measurement(analObj) {
if (window.Bootsrapper._trackAnalytics === function) {
window.Bootstrapper._trackAnalytics(analObj);
}
}
I wrote below code to unit test this function in jest:
import measurement from "../helpers/measurement";
describe('Test measurement', () => {
beforeAll(() => {
const Bootstrapper = {
_trackAnalytics: function(obj) {
return obj;
},
};
window.Bootstrapper = Bootstrapper;
})
test('should send analytics object to rtrack analyitics', () => {
const testObj = {
pageName: "Leave Abasence"
}
const result = measurement(testObj);
expect(testObj).toEqual(result);
})
})
I get "undefined" for result variable that comes from measurement function call as I am unable to make window.measurement._trackAnalytics function available for measurement function at run time.
I would like to know:
Is my approach correct to unit test this scenario? If Yes, How to make the _trackAnalytics function available for measurement function while unit test run time.
Please suggest any other better approach if you know.
The window.measurement._trackAnalytics function is indeed available for measurement function when your test runs. Otherwise, you would get a TypeError for calling something that is not a function.
The problem is that in the measurement method there is nothing being returned. The _trackAnalytics method is called but its result is not returned. That's why you get undefined as a result.
In order to check that it is indeed being called I would use a jest mock function. The test would look like:
test('should send analytics object to rtrack analyitics', () => {
const testObj = {
pageName: 'Leave Abasence'
};
measurement(testObj);
expect(window.Bootstrapper._trackAnalytics).toHaveBeenCalledTimes(1);
expect(window.Bootstrapper._trackAnalytics).toHaveBeenCalledWith(testObj);
});
Note that your code has some problems (which I expect are typos). In the if condition you are checking Bootsrapper instead of Bootstrapper. And you are checking if it is equal to function instead of checking with typeof. I think the line should look:
if (typeof window.Bootstrapper._trackAnalytics === 'function') {
I'm writing a Discord bot that generates weekly Guild stats for text and voice channel usage. My code divides several Mongo queries up into separate methods:
function getTopActiveTextChannels() {
let topTextChannels = []
ChannelModel.find({}).sort({"messageCountThisWeek": -1}).limit(topLimit)
.exec(channels => {
channels.forEach(c => {
topTextChannels.push({"name": c.name, "messageCount": c.messageCount})
})
console.log(topTextChannels)
return topTextChannels
})
}
function getTopActiveVoiceMembers() {
let topVoiceMembers = []
UserModel.find({}).sort({"timeSpentInVoice": -1}).limit(topLimit)
.exec(users => {
users.forEach(u => {
topVoiceMembers.push({"username": u.username, "timeSpentInVoice": u.timeSpentInVoice})
})
console.log(topVoiceMembers)
return topVoiceMembers
})
}
I then have one method that calls both those and (for now) prints the values to console:
function getWeeklyGuildStats(client) {
let topActiveTextChannels = getTopActiveTextChannels()
let topVoiceMembers = getTopActiveVoiceMembers()
let promisesArray = [topActiveTextChannels, topVoiceMembers]
Promise.all(promisesArray).then(values => {console.log(values)})
}
Executing getWeeklyGuildStats(client) outputs: [ undefined, undefined ]. I am sure I'm not using promises correctly, but when I follow Mongoose's documentation, it tells me to use exec() instead of then(), but I get a channels = null error with that.
Does anything jump out to anyone? This seems like a fairly common pattern. Does anyone have a solution for how to resolve multiple Mongoose queries in a single method?
Promise.all should take an array of promises, while your functions are returning normal array, so you need to return the whole query in the helper method which getting the users and channels, then do your logic after the promise.all
your functions may look something like that
function getTopActiveTextChannels() {
return ChannelModel.find({}).sort({"messageCountThisWeek": -1}).limit(topLimit).exec();
}
function getTopActiveVoiceMembers() {
return UserModel.find({}).sort({"timeSpentInVoice": -1}).limit(topLimit).exec();
}
then the function that calls these two methods will be something like
function getWeeklyGuildStats(client) {
let topActiveTextChannels = getTopActiveTextChannels()
let topVoiceMembers = getTopActiveVoiceMembers()
let promisesArray = [topActiveTextChannels, topVoiceMembers]
Promise.all(promisesArray).then(values => {
console.log(values);
// here you could do your own logic, the for loops you did in the helper methods before
});
}
You do not have any return statements in the root level of your functions, so they are always synchronously returning undefined. I'm not familiar with the library you're using, but if for example ChannelModel.find({}).exec(callback) returns a promise with the return value of callback as your code implies, then you just need to add a return statement to your functions.
For example:
function getTopActiveTextChannels() {
let topTextChannels = []
// Return this! (Assuming it returns a promise.) Otherwise you're always returning `undefined`.
return ChannelModel.find({}).sort({"messageCountThisWeek": -1}).limit(topLimit)
.exec(channels => {
channels.forEach(c => {
topTextChannels.push({"name": c.name, "messageCount": c.messageCount})
})
console.log(topTextChannels)
return topTextChannels
})
}
I want to be able to properly test my ES6 class, it's constructor requires another class and all this looks like this:
Class A
class A {
constructor(b) {
this.b = b;
}
doSomething(id) {
return new Promise( (resolve, reject) => {
this.b.doOther()
.then( () => {
// various things that will resolve or reject
});
});
}
}
module.exports = A;
Class B
class B {
constructor() {}
doOther() {
return new Promise( (resolve, reject) => {
// various things that will resolve or reject
});
}
module.exports = new B();
index
const A = require('A');
const b = require('b');
const a = new A(b);
a.doSomething(123)
.then(() => {
// things
});
Since I'm trying to do dependency injection rather than having requires at the top of the classes, I'm not sure how to go about mocking class B and it's functions for testing class A.
Sinon allows you to easily stub individual instance methods of objects. Of course, since b is a singleton, you'll need to roll this back after every test, along with any other changes you might make to b. If you don't, call counts and other state will leak from one test into another. If this kind of global state is handled poorly, your suite can become a hellish tangle of tests depending on other tests.
Reorder some tests? Something fails that didn't before.
Add, change or delete a test? A bunch of other tests now fail.
Try to run a single test or subset of tests? They might fail now. Or worse, they pass in isolation when you write or edit them, but fail when the whole suite runs.
Trust me, it sucks.
So, following this advice, your tests can look something like the following:
const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const b = require('./b');
describe('A', function() {
describe('#doSomething', function() {
beforeEach(function() {
sinon.stub(b, 'doSomething').resolves();
});
afterEach(function() {
b.doSomething.restore();
});
it('does something', function() {
let a = new A(b);
return a.doSomething()
.then(() => {
sinon.assert.calledOnce(b.doSomething);
// Whatever other assertions you might want...
});
});
});
});
However, this isn't quite what I would recommend.
I usually try to avoid dogmatic advice, but this would be one of the few exceptions. If you're doing unit testing, TDD, or BDD, you should generally avoid singletons. They do not mix well with these practices because they make cleanup after tests much more difficult. It's pretty trivial in the example above, but as the B class has more and more functionality added to it, the cleanup becomes more and more burdensome and prone to mistakes.
So what do you do instead? Have your B module export the B class. If you want to keep your DI pattern and avoid requiring the B module in the A module, you'll just need to create a new B instance every time you make an A instance.
Following this advice, your tests could look something like this:
const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const B = require('./b');
describe('A', function() {
describe('#doSomething', function() {
it('does something', function() {
let b = new B();
let a = new A(b);
sinon.stub(b, 'doSomething').resolves();
return a.doSomething()
.then(() => {
sinon.assert.calledOnce(b.doSomething);
// Whatever other assertions you might want...
});
});
});
});
You'll note that, because the B instance is recreated every time, there's no longer any need to restore the stubbed doSomething method.
Sinon also has a neat utility function called createStubInstance which allows you to avoid invoking the B constructor completely during your tests. It basically just creates an empty object with stubs in place for any prototype methods:
const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const B = require('./b');
describe('A', function() {
describe('#doSomething', function() {
it('does something', function() {
let b = sinon.createStubInstance(B);
let a = new A(b);
b.doSomething.resolves();
return a.doSomething()
.then(() => {
sinon.assert.calledOnce(b.doSomething);
// Whatever other assertions you might want...
});
});
});
});
Finally, one last bit of advice that's not directly related to the question-- the Promise constructor should never be used to wrap promises. Doing so is redundant and confusing, and defeats the purpose of promises which is to make async code easier to write.
The Promise.prototype.then method comes with helpful behavior built-in so you should never have to perform this redundant wrapping. Invoking then always returns a promise (which I will hereafter call the 'chained promise') whose state will depend on the handlers:
A then handler which returns a non-promise value will cause the chained promise to resolve with that value.
A then handler which throws will cause the chained promise to reject with the thrown value.
A then handler which returns a promise will cause the chained promise to match the state of that returned promise. So if it resolves or rejects with a value, the chained promise will resolve or reject with the same value.
So your A class can be greatly simplified like so:
class A {
constructor(b) {
this.b = b;
}
doSomething(id) {
return this.b.doOther()
.then(() =>{
// various things that will return or throw
});
}
}
module.exports = A;
I think you're searching for the proxyquire library.
To demonstrate this, I edited a little bit your files to directly include b in a (I did this because of your singleton new B), but you can keep your code, it's just more easy to understand proxyquire with this.
b.js
class B {
constructor() {}
doOther(number) {
return new Promise(resolve => resolve(`B${number}`));
}
}
module.exports = new B();
a.js
const b = require('./b');
class A {
testThis(number) {
return b.doOther(number)
.then(result => `res for ${number} is ${result}`);
}
}
module.exports = A;
I want now to test a.js by mocking the behavior of b. Here you can do this:
const proxyquire = require('proxyquire');
const expect = require('chai').expect;
describe('Test A', () => {
it('should resolve with B', async() => { // Use `chai-as-promised` for Promise like tests
const bMock = {
doOther: (num) => {
expect(num).to.equal(123);
return Promise.resolve('__PROXYQUIRE_HEY__')
}
};
const A = proxyquire('./a', { './b': bMock });
const instance = new A();
const output = await instance.testThis(123);
expect(output).to.equal('res for 123 is __PROXYQUIRE_HEY__');
});
});
Using proxyquire you can easily mock a dependency's dependency and do expectations on the mocked lib. sinon is used to directly spy / stub an object, you have to use generally both of them.
Seems pretty straightforward, since sinon mocks an object by replacing one of its methods with a behavior (as described here):
(I added resolve()-s to both of the promises in your functions to be able to test)
const sinon = require('sinon');
const A = require('./A');
const b = require('./b');
describe('Test A using B', () => {
it('should verify B.doOther', async () => {
const mockB = sinon.mock(b);
mockB.expects("doOther").once().returns(Promise.resolve());
const a = new A(b);
return a.doSomething(123)
.then(() => {
// things
mockB.verify();
});
});
});
Please let me know if I misunderstood something or additional detail what you'd like to test...