이전 글 : [Canvas-불꽃놀이-18] utils.js 단위 테스트
CanvasOption 클래스를 테스트하여 캔버스 크기, 속성 등 검증하기
CanvasOption 환경 설정
CanvasOption 클래스 전체 코드
더보기
import { ANIMATION, SCREEN, POS, FONT } from "@/js/constants.js";
class CanvasOption {
/**
* 캔버스 기본 설정을 관리하고 초기화하는 클래스
* - 캔버스 엘리먼트를 정의하고, 물리적 및 CSS 크기, DPI, 폰트 설정 등을 처리
* - 화면 크기에 따라 캔버스 옵션을 동적으로 조정
*/
constructor() {
this.canvas = document.getElementById("canvas");
if (!this.canvas) {
throw new Error("캔버스 객체를 발견하지 못했습니다. 다시 확인해주세요.");
}
this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
this.fps = ANIMATION.FPS;
this.interval = 1000 / this.fps;
this.initCanvasOptionVars();
}
init() {
this.initCanvasOptionVars();
}
initCanvasOptionVars() {
this.dpr = Math.min(Math.round(window.devicePixelRatio), SCREEN.MAX_DPR) || 1;
this.canvasCssWidth = window.innerWidth;
this.canvasCssHeight = window.innerHeight;
this.isSmallScreen = window.matchMedia(`(max-width: ${SCREEN.SMALL_WIDTH}px)`).matches;
this.mainX = this.canvasCssWidth / POS.MAIN_X_DIVISOR;
this.mainY = Math.floor(this.canvasCssHeight * POS.MAIN_Y_RATIO);
this.mainFontSize = this.setMainFontSize();
this.subFontSize = this.setSubFontSize();
}
/**
* @param {number} [fontSize] 폰트 사이즈, 없을 시 화면 크기에 따라 설정
* @returns {number} 메인 폰트 사이즈 반환 (작은 화면일 경우 별도의 비율 적용)
*/
setMainFontSize(fontSize) {
if (!fontSize) {
const ratio = this.isSmallScreen ? FONT.MAIN_RATIO_SMALL : FONT.MAIN_RATIO_GENERAL;
fontSize = ((this.canvasCssWidth + this.canvasCssHeight) / 2) * ratio;
}
return Math.round(fontSize);
}
/**
* @returns {number} 서브 폰트 사이즈 반환
*/
setSubFontSize() {
return Math.round(this.mainFontSize) * FONT.SUB_RATIO;
}
/**
* @param {number} fontSize
* @returns ctx font 설정을 위한 문자열 반환
*/
setFontStyle(fontSize) {
return `${fontSize}px ${FONT.FAMILY}`;
}
/**
* @param {string} color
*/
fillFullCanvas(color) {
this.ctx.fillStyle = color;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
/**
* @param {number} x
* @param {number} y
* @returns 파티클이 캔버스 영역을 벗어날 경우 true를 반환, 영역안이면 false를 반환
*/
isOutOfCanvasArea(x, y) {
return x < 0 || x > this.canvasCssWidth || y < 0 || y > this.canvasCssHeight;
}
}
export default CanvasOption;
1. jest-canvas-mock 라이브러리 설치
- jest-canvas-mock 라이브러리는 Jest 환경에서 캔버스 관련 기능을 mock function으로 지원하도록 만들어진 라이브러리다.
- 캔버스와 관련된 작업이 많기 때문에 효율적인 테스트를 위해 위 라이브러리를 설치해 테스트를 진행한다.
설치
npm i --save-dev jest-canvas-mock
환경 설정 추가
module.exports = {
// 생략...
// jest-canvas-mock을 Jest 환경에 추가
setupFiles: ["jest-canvas-mock"],
};
2. 공통 세팅 설정하기
- test환경인 jsdom은 브라우저 환경을 가상으로 지원하지만 일부 API(window.matchMedia)는 지원하지 않는다.
window.matckMedia는 모바일 화면인지 PC 화면인지 판별하는 중요한 역할을 담당하므로 Mock 처리를 통해 원하는 동작을 직접 정의해야 한다. - window.matckMedia는 공통적으로 사용되므로 setup.js 파일을 만들어 모든 테스트 파일에서 사용가능하도록 한다.
- 정규식 /\d+/ 는 문자열에서 숫자만 추출한다. (max-width: 480px)가 전달되면 정규식 match 함수를 통해 숫자 배열로 반환된다. 숫자가 1개밖에 없으니 [0]으로 적용한다. 이때 배열의 원소는 string이므로 int로 변환해 준다.
// setup.js
Object.defineProperty(window, "matchMedia", {
writable: true,
// mock 함수로 적용, query : (max-width: 480px) 형식으로 전달된다.
value: jest.fn().mockImplementation((query) => ({
// query를 정규식으로 숫자만 빼서 window.innerWidth와 비교해 모바일인지 pc인지 판별한다.
matches: window.innerWidth <= parseInt(query.match(/\d+/)[0], 10), // 조건에 따라 true/false 반환
})),
});
- jest.config.cjs에 setup.js를 추가해야 전체 test 파일에서 사용할 수 있다. 설정하지 않으면 setup.js의 내용이 동작하지 않는다.
// jest.config.cjs
module.exports = {
// 생략...
// jest-canvas-mock, setup.js 내용을 Jest 환경에 추가
setupFiles: ["jest-canvas-mock", "<rootDir>/__test__/setup.js"],
};
CanvasOption 테스트
1. 상수, 공통 함수 설정
- 테스트에 사용되는 상수는 constants.js 파일에 추가하여 중앙에서 관리하도록 한다.
- 테스트 환경에서 자주 사용하는 속성 변경 함수는 setup.js에 추가해서 관리하도록 한다.
window 크기를 재설정하는 일이 많아 setup.js에 이 속성을 변경하는 함수를 추가하여 사용하도록 한다.
(참고 : 콘솔을 통해서 확인한 jsdom의 window.innerWidth, window.innerHeight의 기본값은 1024, 768이다.)
공통 함수 설정
- setup.js에 윈도우 속성을 변경하는 함수를 추가한다.
// setup.js
/**
* @param {string} property 지정할 속성
* @param {*} value 속성값
*/
export const defineWidowProperty = (property, value) => {
Object.defineProperty(window, property, { writable: true, value });
};
- 원활한 테스트를 위해 window의 innerWidth, innerHeight속성과 TextData 인스턴스의 canvas.width, canvas.height의 속성의 값을 지정해주는 공통 함수를 만들어 사용한다.
// constants.js
export const TEST_OPTION = {
// 생략...
TYPE_INNER_WIDTH: "innerWidth",
TYPE_INNER_HEIGHT: "innerHeight",
INNER_WIDTH: 1000,
SMALL_INNER_WIDTH: 400,
INNER_HEIGHT: 500,
CANVAS_ELEMENT: '<canvas id="canvas"></canvas>',
CANVAS_ID: "canvas",
};
// setup.js
const { CANVAS_ELEMENT, TYPE_INNER_WIDTH, TYPE_INNER_HEIGHT, INNER_WIDTH, INNER_HEIGHT } = TEST_OPTION;
export const setTestCanvas = () => {
// 테스트용 캔버스 추가 (innerHTML로 삽입해야 테스트마다 캔버스가 초기화됨)
document.body.innerHTML = CANVAS_ELEMENT;
// window객체의 innerWidth, innerHeight 커스텀으로 설정
defineWidowProperty(TYPE_INNER_WIDTH, INNER_WIDTH);
defineWidowProperty(TYPE_INNER_HEIGHT, INNER_HEIGHT);
};
2. 테스트 실행 전 공통 설정 추가
- 테스트에 사용할 캔버스 객체와 크기 설정을 공통으로 처리하여, 매 테스트 시 초기화하여 사용하도록 한다.
- fillRect를 사용하여 실제로 호출되었는지, 파라미터 값이 정확한지 확인하기 위해 jest.spyOn을 설정하도록 한다.
(만약 fillStyle과 같은 결괏값 검증만 필요할 경우 jest.spyOn을 호출할 필요는 없다.)
// CanvasOption.test.js
import { defineWidowProperty } from "./setup";
import CanvasOption from "@/js/CanvasOption.js";
import { ANIMATION, SCREEN, POS, FONT, TEST_OPTION } from "@/js/constants.js";
const { TYPE_INNER_WIDTH, TYPE_INNER_HEIGHT, INNER_WIDTH, SMALL_INNER_WIDTH, INNER_HEIGHT } = TEST_OPTION;
describe("CanvasOption 테스트 (jest-canvas-mock 활용)", () => {
let canvasOption;
// 테스트 실행 전 초기화
beforeEach(() => {
// 테스트용 캔버스 생성
setTestCanvas();
// jest-canvas-mock을 통해 CanvasOption 인스턴스 생성
canvasOption = new CanvasOption();
});
// 생략...
});
3. CanvasOption 인스턴스 생성 시 멤버 변수 초기화 테스트
아래의 시나리오대로 멤버 변수 초기화가 잘 이루어지는지 확인한다.
canvas, ctx 초기화
- CanvasOption 생성 시 canvas와 ctx 객체가 제대로 초기화되는지 확인
test("캔버스 객체 관련 멤버변수 초기화", () => {
expect(canvasOption.canvas).toBeInstanceOf(HTMLCanvasElement);
expect(canvasOption.ctx).toBeInstanceOf(CanvasRenderingContext2D);
});
canvas 객체를 불러오지 못할 경우 지정한 에러가 발생하는지 확인
test("캔버스 객체를 불러오지 못할 경우 에러 처리", () => {
// 생성한 캔버스 element 삭제
document.getElementById(TEST_OPTION.CANVAS_ID).remove();
// canvas 객체가 사라져 에러가 발생된다.
expect(() => new CanvasOption()).toThrow("캔버스 객체를 발견하지 못했습니다. 다시 확인해주세요.");
});
fps, interval, dpr 초기화
test("애니메이션 관련 멤버변수 초기화", () => {
expect(canvasOption.fps).toBe(ANIMATION.FPS);
expect(canvasOption.interval).toBe(1000 / canvasOption.fps);
// dpr의 최소값은 1, 최대값은 3이다.
expect(canvasOption.dpr).toBeGreaterThanOrEqual(1);
expect(canvasOption.dpr).toBeLessThanOrEqual(SCREEN.MAX_DPR);
});
캔버스 css 크기, 메인 x, y 좌표 초기화
test("캔버스 css 크기, 메인좌표 관련 멤버변수 초기화", () => {
// css 크기는 window.innerWidth, window.innerHeight으로 결졍된다.
expect(canvasOption.canvasCssWidth).toBe(INNER_WIDTH);
expect(canvasOption.canvasCssHeight).toBe(INNER_HEIGHT);
// css 크기로 main 좌표값을 계산한다.
expect(canvasOption.mainX).toBe(INNER_WIDTH / POS.MAIN_X_DIVISOR);
expect(canvasOption.mainY).toBe(Math.floor(INNER_HEIGHT * POS.MAIN_Y_RATIO));
});
isSmallScreen 초기화
- window.innerWidth가 480px 이하면 모바일 화면이므로 isSmallScreen을 true로 반환하고 481px 이상이면 false를 반환한다.
test("PC 화면일때", () => {
// window.width는 INNER_WIDTH(1,000) 값이므로 481px 이상이므로 false
expect(canvasOption.isSmallScreen).toBe(false);
});
test("모바일 화면일때", () => {
// window.innerWidth를 모바일 화면 사이즈로 변경하고 멤버변수를 초기화 한다.
defineWidowProperty(TYPE_INNER_WIDTH, SMALL_INNER_WIDTH);
canvasOption.initCanvasOptionVars();
// window.width가 480px 이하이므로 true
expect(canvasOption.isSmallScreen).toBe(true);
});
4. fontSize 관련 멤버변수 초기화
- mainFontSize를 모바일/PC 화면일 때, fontsize를 전달하거나 전달하지 않을 때 상황을 가정하여 검증했다.
test.each([
{ fontsize: undefined, testWidth: INNER_WIDTH, notice: "PC 화면-인자 전달하지 않아 기본값 적용" },
{ fontsize: 100, testWidth: SMALL_INNER_WIDTH, notice: "모바일 화면-인자 100 전달" },
])("mainFontSize($fontsize) 테스트 ($notice)", ({ fontsize, testWidth }) => {
// window.innerWidth 재설정
defineWidowProperty(TYPE_INNER_WIDTH, testWidth);
// 멤버 젼수 초기화
canvasOption.initCanvasOptionVars();
// fontsize를 반환받는다.
const result = canvasOption.setMainFontSize(fontsize);
// fontsize를 전달받지 못하면 따로 계산한다.
const ratio = canvasOption.isSmallScreen ? FONT.MAIN_RATIO_SMALL : FONT.MAIN_RATIO_GENERAL;
const expectedResult = fontsize ?? Math.round(((testWidth + INNER_HEIGHT) / 2) * ratio);
// 기대값이 예상값과 맞는지 확인하고 모바일/PC 화면일때 계산 비율이 다르니 isSmallScreen 여부도 같이 확인해주면 좋다.
expect(result).toBe(expectedResult);
expect(canvasOption.isSmallScreen).toBe(testWidth <= SMALL_INNER_WIDTH);
});
- subFontSize는 mainFontSize를 바탕으로 계산하므로 mainFontSize가 검증이 완료된 후 subFontSize를 검증하도록 한다.
test("subFontSize 테스트", () => {
const result = canvasOption.setSubFontSize();
expect(result).toBe(Math.round(canvasOption.mainFontSize * FONT.SUB_RATIO));
});
5. fillFullCanvas 테스트
- fillStyle과 fillRect가 주어진 값들로 잘 동작하는지 확인하도록 한다.
- beforeEach에서 jest.spyOn으로 jest-canvas-mock 객체의 메서드를 사용하게 하도록 조치를 했다.
test("fillFullCanvas 테스트", () => {
canvasOption.fillFullCanvas("#121212");
// ctx의 fillStyle이 전달받은 값으로 설정됐는지 확인
expect(canvasOption.ctx.fillStyle).toBe("#121212");
// fillRect 호출 시 전달된 파라미터 값이 일치하는지 확인
expect(canvasOption.ctx.fillRect).toHaveBeenCalledWith(0, 0, canvasOption.canvas.width, canvasOption.canvas.height);
});
6. isOutOfCanvasArea 테스트
- 인자값이 캔버스 영역 밖에 있으면 true를 반환하고 캔버스 영역 안에 있으면 false를 반환한다.
test.each([
{ x: 10, y: 10, expected: false, notice: "캔버스 내부" },
{ x: INNER_WIDTH, y: INNER_HEIGHT, expected: false, notice: "캔버스 내부" },
{ x: -10, y: 10, expected: true, notice: "캔버스 왼쪽 밖" },
{ x: 10, y: -10, expected: true, notice: "캔버스 위쪽 밖" },
{ x: -10, y: -10, expected: true, notice: "캔버스 왼쪽, 위쪽 밖" },
{ x: INNER_WIDTH + 10, y: 10, expected: true, notice: "캔버스 오른쪽 밖" },
{ x: 10, y: INNER_HEIGHT + 10, expected: true, notice: "캔버스 아래쪽 밖" },
{ x: INNER_WIDTH + 10, y: INNER_HEIGHT + 10, expected: true, notice: "캔버스 오른쪽, 아래쪽 밖" },
])("isOutOfCanvas($x, $y) 테스트 / $notice", ({ x, y, expected }) => {
expect(canvasOption.isOutOfCanvasArea(x, y)).toBe(expected);
});
실행 결과
npx jest ./__test__/canvas/CanvasOption.test.js
Github Repo
'Canvas > 불꽃놀이 프로젝트' 카테고리의 다른 글
[Canvas-불꽃놀이-21] TailParticle 클래스 단위 테스트 (1) | 2024.12.07 |
---|---|
[Canvas-불꽃놀이-20] Particle 클래스 단위 테스트 (1) | 2024.12.06 |
[Canvas-불꽃놀이-18] utils.js 단위 테스트 (0) | 2024.12.04 |
[Canvas-불꽃놀이-17] Jest 설정과 테스트 진행 순서 (1) | 2024.12.03 |
[Canvas-불꽃놀이-16] SparkParticle로 잔상효과 적용하기 (1) | 2024.12.02 |