이전 글 : [Canvas-불꽃놀이-31] Canvas 클래스 테스트 (파티클 업데이트)
애니메이션과 렌더 테스트
Canvas 애니메이션과 렌더 테스트
이제 마지막 관문인 애니메이션과 렌더에 대한 테스트를 진행하겠다.
애니메이션 테스트를 진행할 때 requestAnimationFrame에 관련된 테스트를 하느라 여러 오류가 있었다. 종료 조건을 걸어도 무한루프에 빠진다거나 delta나 그런 값이 생각보다 잘 맞지 않았다.
예전에 실제 코드에서 fps가 설정한 프레임 개수로 카운트 되는지 확인했고, 직접 requestAnimaionFrame을 테스트에서 mocking화 하여 커스터마이징 하는 것보단 업데이트를 진행할 때 호출하는 함수의 호출 횟수를 기준으로 판단하기로 하였다.
Canvas 클래스 애니메이션 관련 코드
더보기
animateFireworks() {
let then = document.timeline.currentTime;
let frameCount = 0;
const bgCleanUp = setRgbaColor(SCREEN.BG_RGB, SCREEN.ALPHA_CLEANUP);
const addFrameCountDivisor = ANIMATION.TAIL_FPS + this.textLength * ANIMATION.TEXT_LEN_MULTIPLIER;
/**
* requestAnimationFrame에 전달할 콜백 함수
* @param {number} now requestAnimationFrame의 timestamp (밀리 초)
*/
const frame = (now) => {
const delta = now - then;
if (delta >= this.interval) {
const alpha = SCREEN.ALPHA_BASE + SCREEN.ALPHA_OFFSET * Math.sin(frameCount / SCREEN.SPEED_CONTROL);
this.fillFullCanvas(setRgbaColor(SCREEN.BG_RGB, alpha));
if (frameCount === 0) {
this.fillFullCanvas(bgCleanUp);
this.createTailParticle();
}
this.updateTailParticle();
this.updateCircleParticle();
this.updateTextParticle();
this.updateSparkParticle();
frameCount = (frameCount + 1) % addFrameCountDivisor;
then = now - (delta % this.interval);
}
this.animationId = requestAnimationFrame(frame);
};
this.animationId = requestAnimationFrame(frame);
}
render() {
if (this.text !== "") {
this.createTextDatas();
this.animateFireworks();
}
}
1-1. 애니메이션 테스트 - 테스트 환경 설정
- 애니메이션이 실행될 때 호출하는 파티클 생성, 업데이트 함수의 호출기록을 감시하기 위해 스파이를 전역에 설정한다.
// CanvasAnimation.test.js
describe("Canvas 클래스 애니메이션 테스트", () => {
let canvasInst;
const userInput = "텍스트 데이터 생성";
let spyFillFullCanvas;
let spyCreateTailParticle;
let spyUpdateTailParticle;
let spyUpdateCircleParticle;
let spyUpdateTextParticle;
let spyUpdateSparkParticle;
// 생략...
}
- 테스트 환경에서 애니메이션을 진행하려면 시간을 테스트환경과 동일하게 맞춰야 한다.
- jest의 useFakeTimers를 통해 날짜, 시간 및 타이머 API를 실제 브라우저 환경의 시간이 아닌 Fake 환경의 타이머를 이용할 수 있게 한다.
- Date, performance.now 등의 시간기준을 Fake Timer에서 가져오도록 하고, jsDom 환경에서 requestAnimationFrame, cancleAnimationFrame 등도 Fake Timer의 시간으로 대체된다.
- 테스트용 캔버스를 생성하고 크기를 설정한다. 시간을 Fake Timer로 교체하고 스파이를 등록한다.
실제 코드에 사용되는 document.timeline은 mock 정의가 되지 않았으므로 따로 정의한다.
// CanvasAnimation.test.js
beforeEach(() => {
// FakeTimers 사용하여 애니메이션 함수 mock 화
jest.useFakeTimers();
// document.timeline mock 설정 추가하고 currentTime을 0으로 설정하여 애니메이션 시작시간을 맞춘다.
defineDomObjectProperty({ domObj: document, property: "timeline", value: { currentTime: 0 } });
// 테스트용 캔버스 생성과 설정
setTestCanvas();
canvasInst = new Canvas();
canvasInst.text = userInput;
canvasInst.textLength = userInput.length;
// 애니메이션 관련 메서드 호출을 확인하기 위한 스파이 등록
spyFillFullCanvas = jest.spyOn(CanvasOption.prototype, "fillFullCanvas").mockImplementation(() => {});
spyCreateTailParticle = jest.spyOn(canvasInst, "createTailParticle").mockImplementation(() => {});
spyUpdateTailParticle = jest.spyOn(canvasInst, "updateTailParticle").mockImplementation(() => {});
spyUpdateCircleParticle = jest.spyOn(canvasInst, "updateCircleParticle").mockImplementation(() => {});
spyUpdateTextParticle = jest.spyOn(canvasInst, "updateTextParticle").mockImplementation(() => {});
spyUpdateSparkParticle = jest.spyOn(canvasInst, "updateSparkParticle").mockImplementation(() => {});
});
- 테스트 후 Fake Timer를 Rear Timer로 교체하고 호출기록도 초기화 하여 다음 테스트에 영향이 가지 않게 한다.
// CanvasAnimaion.test.js
afterEach(() => {
// Fake Timer을 제거하고 본래의 타이머를 사용하게 한다.
jest.useRealTimers();
// 테스트 후 스파이 호출기록을 일괄 초기화 한다.
jest.clearAllMocks();
});
1-2. 애니메이션 테스트 - 글자 수, 프레임별 테스트 진행
- TailParticle의 생성 주기는 글자 수에 따라 달라진다. 메모리 효율성을 위해 문자열이 길수록 생성 주기가 길어진다.
- 프레임 수도 60fps로 설정했으므로 60의 배수로 애니메이션에 사용되는 메서드의 호출 횟수가 동일한지 확인한다.
// CanvasAnimation.test.js
let fpsMultiplier = 1; // fps의 배수를 만드는 숫자
// 테스트 환경
test.each(
["1", "12", "123", "1234", "12345", "123456", "1234567", "12345678", "123456789", "1234567890"].map((s) => ({
userInput: s,
length: s.length,
})),
)("animateFireworks 테스트 | 문자열 길이 : $length | fps: 60의 배수 | 관련 동작 호출 횟수 검증", ({ userInput, length }) => {
// canvas 인스턴스 속성 설정과 데이터 생성
canvasInst.text = userInput;
canvasInst.textLength = length;
canvasInst.init();
canvasInst.createTextDatas();
// 애니메이션이 시작되기 전 animationID가 undefined인지 확인
expect(canvasInst.animationId).toBeUndefined();
canvasInst.animateFireworks();
const fps = 60 * fpsMultiplier++; // fps에 숫자를 곱해 60의 배수로 만든다
// 여기서 frame은 TailParticle 생성을 위해 따로 계산하는 프레임 기준이다.
// tail의 fps를 바탕으로 tail의 생성 횟수를 계산하기 위한 수를 계산한다.
const frameCountDivisor = ANIMATION.TAIL_FPS + length * ANIMATION.TEXT_LEN_MULTIPLIER;
// 위에서 계산한 수를 바탕으로 Tail이 몇번 생성되는지 확인한다.
const fillCanvasAndCreateTailCallCnt = Math.ceil(fps / frameCountDivisor);
// fps * 16.67ms의 시간만큼 애니메이션을 실행시킨다.
jest.advanceTimersByTime(canvasInst.interval * fps);
// 실제로 Fake Timers를 사용할 때는 16ms 마다 프레임이 생성되므로
// 나머지 실행이 안된 시간을 시켜줘야 위에서 설정한 실행시간을 채울 수 있다.
jest.runOnlyPendingTimers();
// fillFullCanvas는 매 프레임마다 호출되면서 tail이 생성될 때 한번더 호출된다.
// 실행 시 color을 넣어서 실행하는지 확인하고
// 프레임 별 호출 횟수 + tail 생성횟수가 총 호출 횟수라고 할 수 있다.
expect(spyFillFullCanvas).toHaveBeenCalledWith(expect.any(String));
expect(spyFillFullCanvas).toHaveBeenCalledTimes(fps + fillCanvasAndCreateTailCallCnt);
// tail 생성 메서드의 호출 횟수가 예상값과 일치하는지 확인
expect(spyCreateTailParticle).toHaveBeenCalledTimes(fillCanvasAndCreateTailCallCnt);
// 업데이트 메서드가 매 프레임마다 호출되는지 확인
expect(spyUpdateTailParticle).toHaveBeenCalledTimes(fps);
expect(spyUpdateCircleParticle).toHaveBeenCalledTimes(fps);
expect(spyUpdateTextParticle).toHaveBeenCalledTimes(fps);
expect(spyUpdateSparkParticle).toHaveBeenCalledTimes(fps);
// 애니메이션 아이디의 값이 할당되었는지 확인
expect(canvasInst.animationId).not.toBeUndefined();
});
2-1. render 테스트 - 테스트 환경 설정
- Canvas 클래스 인스턴스와 render 메서드에서 실행할 메서드의 호출 횟수를 감시하는 스파이를 전역에 선언한다.
// CanvasAnimation.test.js
describe("Canvas 클래스 렌더 테스트", () => {
let canvasInst;
let spyCreateTextData;
let spyAnimateFireworks;
// 생략...
}
- 테스트 실행 전 Canvas 인스턴스를 생성하고 스파이를 등록한다.
// CanvasAnimation.test.js
beforeEach(() => {
// 테스트용 캔버스 생성과 Canvas 클래스 인스턴스 생성
setTestCanvas();
canvasInst = new Canvas();
// 감시할 메서드 스파이 등록
spyCreateTextData = jest.spyOn(canvasInst, "createTextDatas");
spyAnimateFireworks = jest.spyOn(canvasInst, "animateFireworks");
});
2-2. render 테스트 - 테스트 진행
- render 메서드는 사용자 입력 문자열이 Canvas 인스턴스에 저장되지 않으면 실행되지 않는다.
문자열의 유무에 따른 테스트 결과를 예측하도록 한다.
// CanvasAnimation.test.js
test.each([
{ text: "render 테스트", notice: "있음 (문자열 : render 테스트)", called: true },
{ text: "", notice: "없음", called: false },
])("render 테스트 | 사용 문자열: $notice | 텍스트 데이터 생성 & 애니메이션 실행: $called", ({ text }) => {
canvasInst.text = text;
canvasInst.textLength = text.length;
canvasInst.init();
canvasInst.render();
// 문자열 멤버변수의 값이 할당되어 있으면
if (text.length > 0) {
// 텍스트 데이터를 생성하고 애니메이션을 진행한다.
expect(spyCreateTextData).toHaveBeenCalledTimes(1);
expect(spyAnimateFireworks).toHaveBeenCalledTimes(1);
} else {
expect(spyCreateTextData).not.toHaveBeenCalled();
expect(spyAnimateFireworks).not.toHaveBeenCalled();
}
});
테스트 결과
npx jest .__test__/canvas/CanvasAnimation.test.js
Github Repo
'Canvas > 불꽃놀이 프로젝트' 카테고리의 다른 글
[Canvas-불꽃놀이-34] 버그 수정 (인앱 브라우저, ios 하단 높이) (2) | 2024.12.27 |
---|---|
[Canvas-불꽃놀이-33] index.js 통합 테스트 (0) | 2024.12.21 |
[Canvas-불꽃놀이-31] Canvas 클래스 단위 테스트 (파티클 업데이트) (1) | 2024.12.19 |
[Canvas-불꽃놀이-30] Canvas 클래스 단위 테스트 (파티클 생성) (1) | 2024.12.19 |
[Canvas-불꽃놀이-29] Canvas 클래스 단위 테스트 (텍스트 데이터 생성) (1) | 2024.12.17 |