How do I test code that uses `requestAnimationFrame` in jest? - jestjs

I want to write a jest unit test for a module that uses requestAnimationFrame and cancelAnimationFrame.
I tried overriding window.requestAnimationFrame with my own mock (as suggested in this answer), but the module keeps on using the implementation provided by jsdom.
My current approach is to use the (somehow) builtin requestAnimationFrame implementation from jsdom, which seems to use setTimeout under the hood, which should be mockable by using jest.useFakeTimers().
jest.useFakeTimers();
describe("fakeTimers", () => {
test.only("setTimeout and trigger", () => {
const order: number[] = [];
expect(order).toEqual([]);
setTimeout(t => order.push(1));
expect(order).toEqual([]);
jest.runAllTimers();
expect(order).toEqual([1]);
});
test.only("requestAnimationFrame and runAllTimers", () => {
const order: number[] = [];
expect(order).toEqual([]);
requestAnimationFrame(t => order.push(1));
expect(order).toEqual([]);
jest.runAllTimers();
expect(order).toEqual([1]);
});
});
The first test is successful, while the second fails, because order is empty.
What is the correct way to test code that relies on requestAnimationFrame(). Especially if I need to test conditions where a frame was cancelled?

Here solution from the jest issue:
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
});
afterEach(() => {
window.requestAnimationFrame.mockRestore();
});

I'm not sure this solution is perfect but this works for my case.
There are two key principles working here.
1) Create a delay that is based on requestAnimationFrame:
const waitRAF = () => new Promise(resolve => requestAnimationFrame(resolve));
2) Make the animation I am testing run very fast:
In my case the animation I was waiting on has a configurable duration which is set to 1 in my props data.
Another solution to this could potentially be running the waitRaf method multiple times but this will slow down tests.
You may also need to mock requestAnimationFrame but that is dependant on your setup, testing framework and implementation
My example test file (Vue app with Jest):
import { mount } from '#vue/test-utils';
import AnimatedCount from '#/components/AnimatedCount.vue';
const waitRAF = () => new Promise(resolve => requestAnimationFrame(resolve));
let wrapper;
describe('AnimatedCount.vue', () => {
beforeEach(() => {
wrapper = mount(AnimatedCount, {
propsData: {
value: 9,
duration: 1,
formatDisplayFn: (val) => "£" + val
}
});
});
it('renders a vue instance', () => {
expect(wrapper.isVueInstance()).toBe(true);
});
describe('When a value is passed in', () => {
it('should render the correct amount', async () => {
const valueOutputElement = wrapper.get("span");
wrapper.setProps({ value: 10 });
await wrapper.vm.$nextTick();
await waitRAF();
expect(valueOutputElement.text()).toBe("£10");
})
})
});

So, I found the solution myself.
I really needed to override window.requestAnimationFrame and window.cancelAnimationFrame.
The problem was, that I did not include the mock module properly.
// mock_requestAnimationFrame.js
class RequestAnimationFrameMockSession {
handleCounter = 0;
queue = new Map();
requestAnimationFrame(callback) {
const handle = this.handleCounter++;
this.queue.set(handle, callback);
return handle;
}
cancelAnimationFrame(handle) {
this.queue.delete(handle);
}
triggerNextAnimationFrame(time=performance.now()) {
const nextEntry = this.queue.entries().next().value;
if(nextEntry === undefined) return;
const [nextHandle, nextCallback] = nextEntry;
nextCallback(time);
this.queue.delete(nextHandle);
}
triggerAllAnimationFrames(time=performance.now()) {
while(this.queue.size > 0) this.triggerNextAnimationFrame(time);
}
reset() {
this.queue.clear();
this.handleCounter = 0;
}
};
export const requestAnimationFrameMock = new RequestAnimationFrameMockSession();
window.requestAnimationFrame = requestAnimationFrameMock.requestAnimationFrame.bind(requestAnimationFrameMock);
window.cancelAnimationFrame = requestAnimationFrameMock.cancelAnimationFrame.bind(requestAnimationFrameMock);
The mock must be imported BEFORE any module is imported that might call requestAnimationFrame.
// mock_requestAnimationFrame.test.js
import { requestAnimationFrameMock } from "./mock_requestAnimationFrame";
describe("mock_requestAnimationFrame", () => {
beforeEach(() => {
requestAnimationFrameMock.reset();
})
test("reqest -> trigger", () => {
const order = [];
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([]);
requestAnimationFrame(t => order.push(1));
expect(requestAnimationFrameMock.queue.size).toBe(1);
expect(order).toEqual([]);
requestAnimationFrameMock.triggerNextAnimationFrame();
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([1]);
});
test("reqest -> request -> trigger -> trigger", () => {
const order = [];
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([]);
requestAnimationFrame(t => order.push(1));
requestAnimationFrame(t => order.push(2));
expect(requestAnimationFrameMock.queue.size).toBe(2);
expect(order).toEqual([]);
requestAnimationFrameMock.triggerNextAnimationFrame();
expect(requestAnimationFrameMock.queue.size).toBe(1);
expect(order).toEqual([1]);
requestAnimationFrameMock.triggerNextAnimationFrame();
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([1, 2]);
});
test("reqest -> cancel", () => {
const order = [];
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([]);
const handle = requestAnimationFrame(t => order.push(1));
expect(requestAnimationFrameMock.queue.size).toBe(1);
expect(order).toEqual([]);
cancelAnimationFrame(handle);
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([]);
});
test("reqest -> request -> cancel(1) -> trigger", () => {
const order = [];
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([]);
const handle = requestAnimationFrame(t => order.push(1));
requestAnimationFrame(t => order.push(2));
expect(requestAnimationFrameMock.queue.size).toBe(2);
expect(order).toEqual([]);
cancelAnimationFrame(handle);
expect(requestAnimationFrameMock.queue.size).toBe(1);
expect(order).toEqual([]);
requestAnimationFrameMock.triggerNextAnimationFrame();
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([2]);
});
test("reqest -> request -> cancel(2) -> trigger", () => {
const order = [];
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([]);
requestAnimationFrame(t => order.push(1));
const handle = requestAnimationFrame(t => order.push(2));
expect(requestAnimationFrameMock.queue.size).toBe(2);
expect(order).toEqual([]);
cancelAnimationFrame(handle);
expect(requestAnimationFrameMock.queue.size).toBe(1);
expect(order).toEqual([]);
requestAnimationFrameMock.triggerNextAnimationFrame();
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([1]);
});
test("triggerAllAnimationFrames", () => {
const order = [];
expect(requestAnimationFrameMock.queue.size).toBe(0);
expect(order).toEqual([]);
requestAnimationFrame(t => order.push(1));
requestAnimationFrame(t => order.push(2));
requestAnimationFrameMock.triggerAllAnimationFrames();
expect(order).toEqual([1,2]);
});
test("does not fail if triggerNextAnimationFrame() is called with an empty queue.", () => {
requestAnimationFrameMock.triggerNextAnimationFrame();
})
});

Here is my solution inspired by the first answer.
beforeEach(() => {
jest.useFakeTimers();
let count = 0;
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(() => cb(100*(++count)), 100));
});
afterEach(() => {
window.requestAnimationFrame.mockRestore();
jest.clearAllTimers();
});
Then in test mock the timer:
act(() => {
jest.advanceTimersByTime(200);
});
Directly call cb in mockImplementation will produce infinite call loop. So I make use of the Jest Timer Mocks to get it under control.

My solution in typescript. I figured by making time go very quickly each frame, it would make the animations go very (basically instant) fast. Might not be the right solution in certain cases but I'd say this will help many.
let requestAnimationFrameSpy: jest.SpyInstance<number, [callback: FrameRequestCallback]>;
beforeEach(() => {
let time = 0;
requestAnimationFrameSpy = jest.spyOn(window, 'requestAnimationFrame')
.mockImplementation((callback: FrameRequestCallback): number => {
callback(time+=1000000);
return 0;
});
});
afterEach(() => {
requestAnimationFrameSpy.mockRestore();
});

The problem with previous version is that callbacks are called directly, which does not reflect the asynchronous nature of requestAnimationFrame.
Here is a mock which uses jest.useFakeTimers() to achieve this while giving you control when the code is executed:
beforeAll(() => {
jest.useFakeTimers()
let time = 0
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(
// #ts-expect-error
(cb) => {
// we can then use fake timers to preserve the async nature of this call
setTimeout(() => {
time = time + 16 // 16 ms
cb(time)
}, 0)
})
})
afterAll(() => {
jest.useRealTimers()
// #ts-expect-error
window.requestAnimationFrame.mockRestore()
})
in your test you can then use:
yourFunction() // will schedule a requestAnimation
jest.runAllTimers() // execute the callback
expect(....) // check that it happened
This helps to contain Zalgo.

Related

mock a registered resolver in awilix

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');
});
});

mock third party method +jest

I am using jest for my backend unit testing.I need to mock third party library module methods using that.I tried the following code:
My controller file:
const edgejs = require('apigee-edge-js');
const apigeeEdge = edgejs.edge;
async get(req, res) {
const abc= await apigeeEdge.connect(connectOptions);
const Details = await abc.developers.get(options);
return res.status(200).send(Details);
}
test.spec.js
let edgejs = require('apigee-edge-js');
const ctrl = require('../../controller');
describe("Test suite for abc", () => {
test("should return ...", async() =>{
edgejs.edge = jest.fn().mockImplementationOnce(async () =>
{return {"connect":{"developers":{"get":[{}]}}}}
);
ctrl.get(req, res)
});
But its not mocking , its calling the actual library connect method. What i am doing wrong here. Please share your ideas. Thanks in advance.
WORKING CODE
jest.mock('apigee-edge-js', () => {
return { edge: { connect: jest.fn() } };
});
const edgejs = require('apigee-edge-js');
test("should return ...", async () => {
edgejs.edge.connect.mockImplementationOnce(() => Promise.resolve(
{"developers":{"get":[{}]}}
));
edgejs.edge.connect()
expect(edgejs.edge.connect).toBeCalled();
})
ERROR CODE:
jest.mock('apigee-edge-js', () => {
return { edge: { connect: jest.fn() } };
});
const Ctrl = require('../../controllers/controller'); ----> Extra line
const edgejs = require('apigee-edge-js');
test("should return ...", async () => {
edgejs.edge.connect.mockImplementationOnce(() => Promise.resolve(
{"developers":{"get":[{}]}}
));
const req = mockRequest();
const res = mockResponse();
await Ctrl.get(req, res) ---> Extra line
expect(edgejs.edge.connect).toBeCalled();
});
Receceivig erro : TypeError: edgejs.edge.connect.mockImplementationOnce is not a function
The mock doesn't affect anything because controller dereferences edgejs.edge right after it's imported, apigeeEdge = edgejs.edge. This would be different if it were using edgejs.edge.connect instead of apigeeEdge.connect.
Methods shouldn't be mocked as ... = jest.fn() because this prevents them from being restored and may affect other tests after that; this is what jest.spyOn is for. Furthermore, edge is an object and not a method.
Jest provides module mocking functionality. Third-party libraries generally need to be mocked in unit tests.
It should be:
jest.mock('apigee-edge-js', () => {
return { edge: { connect: jest.fn() } };
});
const ctrl = require('../../controller');
const edgejs = require('apigee-edge-js');
test("should return ...", async () => {
edgejs.edge.connect.mockImplementationOnce(() => Promise.resolve(
{"developers":{"get":[{}]}}
));
await ctrl.get(req, res)
...
});

Sinon/sandbox test says that function was never called

I want to write a unit test that checks to see if a function was called, but i'm getting the error:
submitDetails
submitDetails
sendEmail:
AssertionError: expected sendEmail to have been called exactly once, but it was called 0 times
From what I can see my function submitDetails.submitDetails clearly runs the function sendEmail.sendEmail but it's saying that it's never called. I've also tried just using 'spy.called' instead of calledOnce but I get the same result.
Test file:
const submitDetails = require('../src/scripts/submitDetails')
const sendEmail = require('../src/lib/sendEmail')
describe('submitDetails', function () {
let sandbox = null
before(() => {
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
})
describe('submitDetails', () => {
let mockParams, result
beforeEach(async () => {
sandbox.spy(sendEmail, 'sendEmail')
})
it('sendEmail', () => {
expect(sendEmail.sendEmail).to.have.been.calledOnce()
})
})
})
SubmitDetails.js (file that's being test)
const { sendEmail } = require('../lib/sendEmail')
const submitDetails = {}
submitDetails.submitDetails = query => {
return sendEmail(query)
}
module.exports = submitDetails
You didn't call submitDetails.submitDetails() method in your test case. Here is the working example:
sendEmail.ts:
module.exports = {
sendEmail() {}
};
submitDetails.ts:
const sendEmail = require('./sendEmail');
// #ts-ignore
const submitDetails = {};
// #ts-ignore
submitDetails.submitDetails = query => {
return sendEmail.sendEmail(query);
};
module.exports = submitDetails;
submitDetails.spec.ts:
import { expect } from 'chai';
import sinon, { SinonSandbox, SinonSpy } from 'sinon';
const submitDetails = require('./submitDetails');
const sendEmail = require('./sendEmail');
describe('submitDetails', () => {
let sandbox: SinonSandbox;
before(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
describe('submitDetails', () => {
let sendEmailSpy: SinonSpy;
beforeEach(() => {
sendEmailSpy = sandbox.spy(sendEmail, 'sendEmail');
});
it('sendEmail', () => {
submitDetails.submitDetails();
sandbox.assert.calledOnce(sendEmailSpy);
expect(sendEmailSpy.calledOnce).to.be.true;
});
});
});
Unit test result:
submitDetails
submitDetails
✓ sendEmail
1 passing (22ms)
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/58058653

Understand the utility of mocks with Jest

I can't understand at all the utility of mockings. See, I have the next module:
function Code() {
this.generate = () => {
const result = 'code124';
return result;
};
}
module.exports = Code;
Now, I want to test it with jest:
const Code = require('../lib/code');
jest.mock('../lib/code', () => {
return jest.fn().mockImplementation(() => {
return {
generate: () => [1, 2, 3]
};
});
});
describe('Code', () => {
test('returns the code "code123"', () => {
const code = new Code();
expect(code.generate()).toBe('code123');
});
});
So... This test will be fine but... My code ain't so... what's the point about mocking if I can set a correct result even though my code is wrong?
You're NOT supposed to mock the unit you're testing. You're supposed to mock it's dependencies.
for example:
whenever you have a dependency in the implementation:
const dependency = require('dependency');
function Code() {
this.generate = () => {
const result = 'code' + dependency.getNumber();
return result;
};
}
module.exports = Code;
you'll be able to modify it's results to be able to test all scenarios without using the actual implementation of your dependency:
const dependency = require('dependency');
const Code = require('../lib/code');
jest.mock('dependency');
describe('Code', () => {
describe('when dependency returns 123', () => {
beforeAll(() => {
dependency.getNumber.mockReturnValue('123');
});
it('should generate code123', () => {
const code = new Code();
expect(code.generate()).toEqual('code123');
});
});
describe('when dependency returns 124', () => {
beforeAll(() => {
dependency.getNumber.mockReturnValue('124');
});
it('should generate code123', () => {
const code = new Code();
expect(code.generate()).toEqual('code124');
});
});
});

Tracking context with async_hooks

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());
});
});
});
});

Resources