如何测试开玩笑地使用“requestAnimationFrame”的代码?
How do I test code that uses `requestAnimationFrame` in jest?
我想为使用 requestAnimationFrame
和 cancelAnimationFrame
的模块编写一个有趣的单元测试。
我尝试用我自己的 mock 覆盖 window.requestAnimationFrame(如 中所建议),但该模块继续使用 jsdom 提供的实现。
我目前的方法是使用来自 jsdom 的(以某种方式)内置 requestAnimationFrame
实现,它似乎在幕后使用 setTimeout
,应该可以通过使用 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]);
});
});
第一次测试成功,第二次失败,因为order
是空的。
测试依赖于 requestAnimationFrame()
的代码的正确方法是什么?特别是如果我需要测试帧被取消的情况?
我不确定这个解决方案是否完美,但这适用于我的情况。
这里有两个关键原则。
1) 创建一个基于requestAnimationFrame的延迟:
const waitRAF = () => new Promise(resolve => requestAnimationFrame(resolve));
2) 使我正在测试的动画运行非常快:
在我的例子中,我正在等待的动画有一个可配置的持续时间,在我的道具数据中设置为 1。
另一种解决方案可能是 运行多次调用 waitRaf 方法,但这会减慢测试速度。
您可能还需要模拟 requestAnimationFrame,但这取决于您的设置、测试框架和实现
我的示例测试文件(带有 Jest 的 Vue 应用程序):
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");
})
})
});
所以,我自己找到了解决方案。
我真的需要覆盖 window.requestAnimationFrame
和 window.cancelAnimationFrame
。
问题是我没有正确包含模拟模块。
// 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);
必须先导入 mock BEFORE 导入任何可能调用 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();
})
});
这里是开玩笑的解决方案issue:
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
});
afterEach(() => {
window.requestAnimationFrame.mockRestore();
});
这是我的解决方案,灵感来自第一个答案。
beforeEach(() => {
jest.useFakeTimers();
let count = 0;
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(() => cb(100*(++count)), 100));
});
afterEach(() => {
window.requestAnimationFrame.mockRestore();
jest.clearAllTimers();
});
然后在测试中模拟计时器:
act(() => {
jest.advanceTimersByTime(200);
});
在mockImplementation
中直接调用cb
会产生死循环。所以我使用 Jest Timer Mocks 来控制它。
我想为使用 requestAnimationFrame
和 cancelAnimationFrame
的模块编写一个有趣的单元测试。
我尝试用我自己的 mock 覆盖 window.requestAnimationFrame(如
我目前的方法是使用来自 jsdom 的(以某种方式)内置 requestAnimationFrame
实现,它似乎在幕后使用 setTimeout
,应该可以通过使用 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]);
});
});
第一次测试成功,第二次失败,因为order
是空的。
测试依赖于 requestAnimationFrame()
的代码的正确方法是什么?特别是如果我需要测试帧被取消的情况?
我不确定这个解决方案是否完美,但这适用于我的情况。
这里有两个关键原则。
1) 创建一个基于requestAnimationFrame的延迟:
const waitRAF = () => new Promise(resolve => requestAnimationFrame(resolve));
2) 使我正在测试的动画运行非常快:
在我的例子中,我正在等待的动画有一个可配置的持续时间,在我的道具数据中设置为 1。
另一种解决方案可能是 运行多次调用 waitRaf 方法,但这会减慢测试速度。
您可能还需要模拟 requestAnimationFrame,但这取决于您的设置、测试框架和实现
我的示例测试文件(带有 Jest 的 Vue 应用程序):
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");
})
})
});
所以,我自己找到了解决方案。
我真的需要覆盖 window.requestAnimationFrame
和 window.cancelAnimationFrame
。
问题是我没有正确包含模拟模块。
// 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);
必须先导入 mock BEFORE 导入任何可能调用 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();
})
});
这里是开玩笑的解决方案issue:
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
});
afterEach(() => {
window.requestAnimationFrame.mockRestore();
});
这是我的解决方案,灵感来自第一个答案。
beforeEach(() => {
jest.useFakeTimers();
let count = 0;
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(() => cb(100*(++count)), 100));
});
afterEach(() => {
window.requestAnimationFrame.mockRestore();
jest.clearAllTimers();
});
然后在测试中模拟计时器:
act(() => {
jest.advanceTimersByTime(200);
});
在mockImplementation
中直接调用cb
会产生死循环。所以我使用 Jest Timer Mocks 来控制它。