[Canvas-불꽃놀이-19] CanvasOption 단위 테스트

이전 글 : [Canvas-불꽃놀이-18] utils.js 단위 테스트

 

[Canvas-불꽃놀이-18] utils.js 단위 테스트

이전 글 : [Canvas-불꽃놀이-17] Jest 설정과 테스트 진행 순서 [Canvas-불꽃놀이-17] Jest 설정과 테스트 진행 순서이전 글 : [Canvas-불꽃놀이-16] SparkParticle로 잔상효과 적용하기 [Canvas-불꽃놀이-16] SparkPa

jinsk-joy.tistory.com

 

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
 

GitHub - jinsk9268/text-fireworks

Contribute to jinsk9268/text-fireworks development by creating an account on GitHub.

github.com