I'm trying to understand how to correctly unit test a custom debounce method we have:
// function we want to test
function debounce(func, wait = 100) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
};
}
Here is the Jest unit test that is failing:
describe('DebounceExample', () => {
beforeAll(jest.useFakeTimers);
afterAll(jest.useRealTimers);
it('should debounce', () => {
// given
const data = 'Chuck Norris is faster than you think';
const debounceTime = 5000;
const callback = jest.fn();
const debouncedFunction = debounce(callback, debounceTime);
// when
debouncedFunction(data);
// then
expect(callback).not.toHaveBeenCalled();
// when
jest.runAllTimers(); // jest.advanceTimersByTime(debounceTime);
// then
expect(callback).toHaveBeenCalled();
});
});
Failure:
Error: expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0 Jest
I've also tried the solution here: https://stackoverflow.com/a/52638566/704681 without success
Workaround: the only way I'm able to test it so far:
it('should denounce - workaround', (done) => {
// given
const data = 'Chuck Norris is faster than you think';
const debounceTime = 5000;
const callback = jest.fn((param) => {
// then
expect(param).toEqual(data);
done();
});
const debouncedFunction = debounce(callback, debounceTime);
// when
debouncedFunction(data);
// then
expect(callback).not.toHaveBeenCalled();
// when (see 'then' inside callback implementation)
jest.runAllTimers(); // jest.advanceTimersByTime(debounceTime);
});
Related
I am trying to check the number of times a function has been called and I am receiving this error
expect(waitForCallStatus(callService, currentCall, CallStatus.Held, delay, retries, nextAttempt)).toHaveBeenCalledTimes(2);
The error -
expect(received).toHaveBeenCalledTimes(expected)
Matcher error: received value must be a mock or spy function
Here is my test file - In the test I am trying to check the recursion behaviour if in first attempt the status isn't matched it should try again
describe('lib.call-status-service.spec.ts', () => {
const callService = getMockCallService();
let holdCall: Call;
let currentCall: Call;
const delay = 300;
const retries = 5;
beforeEach(() => {
jest.clearAllMocks();
holdCall = getMockTenfoldCall({
_id: '1234',
pbxCallId: '1234',
status: CallStatus.Held,
});
currentCall = getMockTenfoldCall({
_id: '4321',
pbxCallId: '4321',
status: CallStatus.Connected,
});
(waitForCallStatus as jest.Mock).mockResolvedValue({});
});
describe('waitCallForStatus', () => {
it('should check if the fetched call status matches the expected status incase of holdCall', () => {
const status = 'Held';
(callService.findCall as jest.Mock).mockResolvedValue([holdCall]);
expect(holdCall.status).toEqual(status);
expect(waitForCallStatus(callService, currentCall, CallStatus.Held)).resolves.toHaveReturnedWith(holdCall);
});
it('Should throw an error if current retry is equal to total number of retries', () => {
const status = 'Held';
(callService.findCall as jest.Mock).mockResolvedValue([currentCall]);
expect(currentCall.status).not.toEqual(status);
const nextAttempt = 5;
expect(waitForCallStatus(callService, currentCall, CallStatus.Held, delay, retries, nextAttempt))
.rejects.toThrowError('Max Retries Reached');
});
it('Should check again if in first attemp fetched call status doesnt match the expected status', () => {
const status = 'Held';
(callService.findCall as jest.Mock).mockResolvedValue([currentCall]);
expect(currentCall.status).not.toEqual(status);
const nextAttempt=1;
expect(waitForCallStatus(callService, currentCall, CallStatus.Held, delay, retries, nextAttempt)).toHaveBeenCalledTimes(2);
});
});
});
Here is my function:-
callService: CallService,
call: Call,
status: CallStatus,
delay = 300,
retries = 5,
currentTry = 0,
) :Promise<any> {
if (currentTry === retries) {
throw TfApiError.badRequest('Max retries reached');
}
await Bluebird.delay(delay);
const query = {
_id: call._id,
pbxCallId: call.pbxCallId,
};
const updatedCall = await callService.findCall(query);
if (updatedCall.status === status) {
return call;
}
const nextAttempt = currentTry + 1;
return waitForCallStatus(callService, updatedCall, status, delay, retries, nextAttempt);
}
What am I doing wrong?
I am new to using Jest for unit tests. How can I mock this simple http request method "getData"? Here is the class:
const got = require("got")
class Checker {
constructor() {
this.url
this.logData = this.logData.bind(this);
this.getData = this.getData.bind(this);
}
async getData(url) {
const response = await got(url);
const data = await response.body;
return data;
}
async logData(first, second, threshold) {
let data = await this.getData(this.url)
console.log("received " + data.body);
}
}
I am trying to mock "getData" so I can write a unit test for "logData". Do I need to mock out the entire "got" module? Thanks.
If you change invoking got to got.get you should be able to have a working test like so:
const got = require('got');
const Checker = require('../index.js');
describe("some test", () => {
beforeEach(() => {
jest.spyOn(got, 'get').mockResolvedValue({ response: { body: { somekey: "somevalue" } } } );
});
it("works", async () => {
new Checker().getData();
expect(got.get).toBeCalledTimes(1);
})
})
One approach is to use dependency injection. Instead of calling 'got' directly, you can 'ask for it' in the class constructor and assign it to a private variable. Then, in the unit test, pass a mock version instead which will return what you want it to.
const got = require("got");
class Checker {
constructor(gotService) {
this.got = gotService;
this.logData = this.logData.bind(this);
this.getData = this.getData.bind(this);
}
async getData(url) {
const response = await this.got(url);
const data = await response.body;
return data;
}
async logData(first, second, threshold) {
let data = await this.getData(this.url)
console.log("received " + data.body);
}
}
//real code
const real = new Checker(got);
//unit testable code
const fakeGot = () => Promise.resolve(mockedData);
const fake = new Checker(fakeGot);
Here is what we are doing:
'Inject' got into the class.
In the class, call our injected version instead of directly calling the original version.
When it's time to unit test, pass a fake version which does what you want it to.
You can include this directly inside your test files. Then trigger the test that makes the Http request and this will be provided as the payload.
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: { eth: 0.6, btc: 0.02, ada: 1 } }),
})
);
it('should return correct mock token values', async () => {
const addresses = ["mockA", "mockB", "mockC"];
const res = await getTokenData(addresses);
expect(res.data).toEqual({ eth: 0.6, btc: 0.02, ada: 1 });
});
Trying to solve a problem where I can't seem to pass dynamically gathered data to Mocha tests.
Here is the logic of my application:
Client submits their Github url. Request is made to Express/Node application.
Express/Node application takes repo and username and makes request to Github API for data and adds the content of the files to an object as base64.
The object with the files are passed to the relevant test files and then executed.
The results are processed and preliminary grades are created. These are then sent back to the client.
Here is what a test file can look like:
const chai = require('chai');
const chaiSubset = require('chai-subset');
chai.use(chaiSubset);
const expect = chai.expect;
const base64 = require('base-64');
const HTML_CONTENT = require('../../00-sandbox-files/basic-portfolio-solution.json').html;
const CSS_CONTENT = require('../../00-sandbox-files/basic-portfolio-solution.json').css;
const decodedCSS = base64.decode(CSS_CONTENT[1].content);
const cheerio = require('cheerio');
const juice = require('juice');
let decodedHTMLcontact;
let decodedHTMLindex;
let decodedHTMLportfolio;
for (const obj in HTML_CONTENT) {
if (HTML_CONTENT[obj].path == "contact.html") {
decodedHTMLcontact = base64.decode(HTML_CONTENT[obj].content);
} else if (HTML_CONTENT[obj].path == "index.html") {
decodedHTMLindex = base64.decode(HTML_CONTENT[obj].content);
} else if (HTML_CONTENT[obj].path == "portfolio.html") {
decodedHTMLportfolio = base64.decode(HTML_CONTENT[obj].content);
}
}
tests = function (html, css) {
describe('HTML Elements tests that should pass for contact.html', function () {
let $ = cheerio.load(decodedHTMLcontact);
describe('HTML Elements that should exist in contact.html', function () {
it('should contain a header element', function () {
expect($('body').find('header').length).to.equal(1);
});
it('should contain a section element', function () {
expect($('body').find('section').length).to.equal(1);
});
it('should contain several anchor elements', function () {
expect($('nav').find('a').length).to.be.at.least(3, 'You need an additional anchor elements for your navigation elements');
});
it('should contain an h1 element', function () {
expect($('body').find('h1').length).to.equal(1);
});
it('should contain a form element', function () {
expect($('body').find('form').length).to.equal(1);
});
it('should contain a footer element', function () {
expect($('body').find('footer').length).to.equal(1);
});
});
Here is the execution file for the Mocha tests:
const Mocha = require('mocha');
// Instantiate a Mocha instance.
const mocha = new Mocha();
const HW_PORTFOLIO_PATH = './server/05-run-testing-suite/HW-Week1-portfolio-wireframe/HW-1-portfolio.js';
function homeworkRouter(contentObj, repo) {
switch (repo) {
case "Basic-Portfolio":
mocha.addFile(HW_PORTFOLIO_PATH);
break;
case "HW-Wireframe":
mocha.addFile('./server/05-run-testing-suite/HW-Week1-portfolio-wireframe/HW-1-wireframe.js');
break;
default:
console.log("No homework provided");
break;
}
}
module.exports = {
// Run the tests and have info about what can be returned
runTests: function(contentObj, repo) {
homeworkRouter(contentObj, repo);
console.log("Content Object", contentObj);
console.log("Repo", repo);
mocha.run()
.on('fail', function (test, err) {
console.log('Test fail');
console.log(err);
})
.on('end', function () {
console.log('All done');
});
}
}
Solutions we've come up with involve using vm(), setting data into globals, and/or building up and tearing down files. I'd like a solution that it's much more efficient and doesn't pollute global.
I'm trying to get sinon.stub to work for async function. I have created promiseFunction.js:
let functionToBeStubbed = async function() {
return ("Text to be replaced by stub.");
};
let promiseFunction = async function() {
return(await functionToBeStubbed());
};
module.exports = {
promiseFunction: promiseFunction,
functionToBeStubbed: functionToBeStubbed
};
and test promiseFunction.spec.js:
let functionstobestested = require('./promiseFunction.js');
describe('Sinon Stub Test', function () {
var sandbox;
it('should return --Text to be replaced by stub.--', async function () {
let responsevalue = "The replaced text.";
sandbox = sinon.sandbox.create();
sandbox.stub(functionstobestested, 'functionToBeStubbed').resolves(responsevalue);
//sandbox.stub(functionstobestested, 'functionToBeStubbed').returns(responsevalue);
let result = "Empty";
console.log(`BEFORE: originaldata = ${result}, value = ${responsevalue}`);
result = await functionstobestested.promiseFunction();
console.log(`AFTER: originaldata = ${result}, value = ${responsevalue}`);
expect(result).to.equal(responsevalue);
sandbox.restore();
console.log("AFTER2: Return value after restoring stub: " + await functionstobestested.promiseFunction());
});
});
when running the test, I will get
test failure
If I modify export slightly, it still fails:
var functionsForTesting = {
promiseFunction: promiseFunction,
functionToBeStubbed: functionToBeStubbed
};
module.exports = functionsForTesting;
I do not understand why this test fails, as it should pass. If I change the way I export functions from promiseFunction.js - module, the test pass correctly. Revised promiseFunction.js:
const functionsForTesting = {
functionToBeStubbed: async function() {
return ("Text to be replaced by stub.");
},
promiseFunction: async function() {
return(await functionsForTesting.functionToBeStubbed());
};
module.exports = functionsForTesting;
Test pass
What's wrong in my original and modified way to export functions?
Using proxyquire I'm mocking a method of module B (injected with require() in module A), when testing the method in the module A. The mock (mocking get_campaigns method of admitad.model.js module):
const admitadModelMock = {
'../services/admitad.model': {
get_campaigns: (limit, page) => new Promise((resolve, reject) =>
setTimeout(resolve({campaigns: testData, count: 1000}), 5000)
),
},
};
The test:
it('shold get all campaigns from Admitad', async function () {
this.timeout(60000);
let err, data;
// mock dependencie (get_campaigns() of module B will be mocked):
let $serviceStubbed = proxyquire('../services/campaign-sync', admitadModelMock);
// getAdmitadCampaigns() just calls get_campaigns method of module B
[err, data] = await to($serviceStubbed.getAdmitadCampaigns(50));
data.length.should.be.equal(50);
});
The problem is the test passing without expected delay of 5 seconds.
Update.
This is working:
setTimeout(() => resolve({campaigns: mockedCampaigns, count: 1000}), 2000)
Here is my final nice approach:
// helper to wrap timeout generation
const timer = (data, time) =>
new Promise((resolve, reject) =>
setTimeout(() => resolve(data), time)
);
// Factory function to generate mock with data we need
const blacklistedModelMockFactory =
(onRead = [], onUpdate = 'Ok', onCreate = 'Ok', onDelete = 'Ok') => ({
'../services/campaigns-blacklist.model': {
read: () => timer(onRead, 2000),
update: () => timer(onUpdate, 2000),
create: () => timer(onCreate, 1000),
delete: () => timer(onDelete, 1000),
},
});
// Test example
it('should filter Registered and Blacklisted collections', async function () {
this.timeout(60000);
$service.should.have.property('filterRB').a('function');
const sourceRegistered = mockedCampaigns.slice(5, 10);
const sourceBlacklisted = mockedCampaigns.slice(15, 18);
let error, success;
// mock dependencies in tested module with our expected data:
let $serviceStubbed = proxyquire(
'../services/campaign-sync', Object.assign(
{},
blacklistedModelMockFactory(sourceBlacklisted),
registeredModelMockFactory(sourceRegistered)
)
);
[error, success] = await to($serviceStubbed.filterRB(mockedCampaigns));
expect(error).to.be.equal(null);
success.filtered.length.should.be.equal(12);
success.blacklisted.length.should.be.equal(3);
success.registered.length.should.be.equal(5);
});
setTimeout(resolve({campaigns: testData, count: 1000}), 5000)
The above line call flow can be explained as below.
let res = resolve({campaigns: testData, count: 1000});
setTimeout(res, 5000);
You don't want that, do you :-)
Try,
setTimeout(() => resolve({ campaigns: testData, count: 1000 }), 5000)
as it wraps the resolve call inside an anonymous function and passes that as 1st parameter to setTimeout call.