[Canvas-불꽃놀이-31] Canvas 클래스 단위 테스트 (파티클 업데이트)

이전 글 : [개발/Canvas] - [Canvas-불꽃놀이-30] Canvas 클래스 테스트 (파티클 생성)

 

[Canvas-불꽃놀이-30] Canvas 클래스 테스트 (파티클 생성)

이전 글 : [Canvas-불꽃놀이-29] Canvas 클래스 단위 테스트 (텍스트 데이터 생성) [Canvas-불꽃놀이-29] Canvas 클래스 단위 테스트 (텍스트 데이터 생성)이전 글 : [Canvas-불꽃놀이-28] Canvas 클래스 단위 테

jinsk-joy.tistory.com

 

Tail, Text, Circle, Spark 파티클 업데이트 테스트

 

파티클 업데이트 테스트

파티클 생성에 대한 테스트를 진행했으니 이제 업데이트에 대한 테스트를 진행하도록 한다.

각 파티클 타입의 업데이트가 의도한 대로 이루어지는지 검증한다.

 

Canvas 파티클 업데이트 코드

더보기
updateTailParticle() {
    for (let i = this.tailParticles.length - 1; i >= 0; i--) {
        const tail = this.tailParticles[i];
        tail.update();
        tail.draw();

        const sparkQty = Math.round(Math.abs(tail.vy * SPARK.TAIL_CREATION_RATE));
        for (let i = 0; i < sparkQty; i++) {
            const sparkParams = {
                x: tail.x,
                y: tail.y,
                vx: randomFloat(SPARK.TAIL_MIN_VX, SPARK.TAIL_MAX_VX),
                vy: randomFloat(SPARK.TAIL_MIN_VY, SPARK.TAIL_MAX_VY),
                opacity: randomFloat(SPARK.TAIL_MIN_OPACITY, SPARK.TAIL_MAX_OPACITY),
                color: tail.fillColor,
            };
            this.sparkParticles.push(this.pm.acquireParticle(TYPE_SPARK, sparkParams));
        }

        if (tail.belowOpacityLimit(TAIL.OPACITY_LIMIT)) {
            this.createTextParticle(tail.x, tail.y);
            this.createCircleParticle(tail.x, tail.y);

            this.tailParticles.splice(i, 1);
            this.pm.returnToPool(TYPE_TAIL, tail);
        }
    }
}

updateTextParticle() {
    for (let i = this.textParticles.length - 1; i >= 0; i--) {
        const text = this.textParticles[i];
        text.update();
        text.draw();

        if (text.belowOpacityLimit() || this.isOutOfCanvasArea(text.x, text.y)) {
            this.textParticles.splice(i, 1);
            this.pm.returnToPool(TYPE_TEXT, text);
        }
    }
}

updateCircleParticle() {
    for (let i = this.circleParticles.length - 1; i >= 0; i--) {
        const circle = this.circleParticles[i];
        circle.update();
        circle.draw();

        if (Math.random() < SPARK.CIRCLE_CREATION_RATE * this.textLength) {
            const sparkParams = {
                x: circle.x,
                y: circle.y,
                vx: randomFloat(SPARK.CIRCLE_MIN_VX, SPARK.CIRCLE_MAX_VX),
                vy: SPARK.CIRCLE_VY,
                color: circle.fillColor,
                radius: circle.radius,
                opacity: circle.opacity + SPARK.CIRCLE_OPACITY_OFFSET,
            };
            this.sparkParticles.push(this.pm.acquireParticle(TYPE_SPARK, sparkParams));
        }

        if (circle.belowOpacityLimit() || this.isOutOfCanvasArea(circle.x, circle.y)) {
            this.circleParticles.splice(i, 1);
            this.pm.returnToPool(TYPE_CIRCLE, circle);
        }
    }
}

updateSparkParticle() {
    for (let i = this.sparkParticles.length - 1; i >= 0; i--) {
        const spark = this.sparkParticles[i];
        spark.update();
        spark.draw();

        if (spark.belowOpacityLimit() || this.isOutOfCanvasArea(spark.x, spark.y)) {
            this.sparkParticles.splice(i, 1);
            this.pm.returnToPool(TYPE_SPARK, spark);
        }
    }
}

 

1. 테스트 환경 공통 설정

  • 캔버스 인스턴스, 풀에 반환 메서드 호출 감시를 위한 스파이 등 테스트에 필요한 변수를 전역에서 선언한다.
// CanvasUpdateParticle.test.js
describe("Canvas 클래스 파티클 업데이트 테스트", () => {
    let canvasInst; // 테스트할 Canvas 클래스 인스턴스
    const userInput = "텍스트 데이터 생성";
    const { INNER_WIDTH, INNER_HEIGHT } = TEST_OPTION;

    let spyReturnToPool; // 풀에 반환하는 메서드의 호출 감시

    // 생략...
}

  • 매번 테스트를 진행하기 전 새로운 테스트용 캔버스를 생성하고 크기를 설정한다. 
    풀에 반환을 감시하는 스파이를 등록한다.
    테스트할 Canvas 클래스의 인스턴스를 생성하고 공통적으로 쓰이는 값을 설정한다.
  • 테스트가 끝난 후에는 스파이로 등록한 메서드의 호출 기록을 일괄적으로 초기화하여 다음 테스트에 영향이 가지 않도록 한다.
// CanvasUpdateParticle.test.js

// 테스트 진행 전
beforeEach(() => {
    // 테스트용 Canvas 생성과 크기 설정
    setTestCanvas();

    // 메서드 호출 감시를 위한 스파이 등록
    spyReturnToPool = jest.spyOn(ParticleManager.prototype, "returnToPool");

    // 테스트할 클래스의 인스턴스를 생성하고 공통값 세팅
    canvasInst = new Canvas();
    canvasInst.text = userInput;
    canvasInst.textLength = userInput.length;
});

// 테스트 진행 후 등록한 스파이의 호출 기록 일괄적으로 초기화
afterEach(() => {
    jest.clearAllMocks(); 
});

  • 파티클을 업데이트한 후 업데이트 로직이 정확히 실행됐는지 검증하는 공통 함수를 만든다.
// CanvasUpdateParticle.test.js
/**
 * @param {jest.SpyInstance} spyUpdate 파티클 update 메서드 스파이
 * @param {jest.SpyInstance} spyDraw 파티클 draw 메서드 스파이
 * @param {Array} particleArr 파티클 배열
 * @param {number} length 배열 길이
 */
function expectAfterParticleUpdate(spyUpdate, spyDraw, particleArr, length) {
    // 파티클 update, draw 메서드가 호출 되었는지 검증
    expect(spyUpdate).toHaveBeenCalledTimes(1);
    expect(spyDraw).toHaveBeenCalledTimes(1);
    
    // 업데이트 후 파티클 배열의 길이가 예상값과 일치하는지 검증
    expect(particleArr).toHaveLength(length);
    
    // 파라미터 값에 따라 풀에 반환하는 메서드 호출 여부 검증
    length === 0 ? expect(spyReturnToPool).toHaveBeenCalledTimes(1) : expect(spyReturnToPool).not.toHaveBeenCalled();
}

2. TailParticle 업데이트 테스트

  • 테스트 실행 전 공통으로 쓰는 변수와 설정을 세팅한다.
// CanvasUpdateParticle.test.js
describe("TailParticle 업데이트 테스트", () => {

    // TailParticle을 비활성화할 때 Text와 Circle 파티클을 생성하므로 호출 감시를 위한 스파이 선언
    let spyCreateTextParticle;
    let spyCreateCircleParticle;
    
    // 실제로 Tail이 업데이트되고 화면에 그려지는지 감시하기 위한 스파이 선언
    let spyTailUpdate;
    let spyTailDraw;

    // 테스트 전 위에서 선언한 스파이를 등록한다.
    beforeEach(() => {
        spyCreateTextParticle = jest.spyOn(canvasInst, "createTextParticle");
        spyCreateCircleParticle = jest.spyOn(canvasInst, "createCircleParticle");
        spyTailUpdate = jest.spyOn(TailParticle.prototype, "update");
        spyTailDraw = jest.spyOn(TailParticle.prototype, "draw");
    });
    
    // 생략...
}

  • Tail의 opacity가 기준값 이하면 Tail은 파티클 배열에서 제외되고 풀에 리턴된다. 이 과정에서 Text, Circle 파티클이 생성되므로 의도한 대로 동작하는지 확인한다.
  • Tail의 투명도가 기준값 초과, 기준값 이하일 때 상황을 나눠서 테스트를 진행한다.
test.each([
    // 테스트 상황 세팅 (tail의 opacity는 vy를 따라간다)
    { vy: -10, length: 1, notice: "이상" }, 
    { vy: -0.001, length: 0, notice: "미만" },
])("updateTailParticle 테스트 | opacity 상한선 $notice", ({ vy, length }) => {
    canvasInst.init();
    canvasInst.tailParticles.push(
        new TailParticle({ ctx: canvasInst.ctx, isSmallScreen: canvasInst.isSmallScreen, x: 100, y: INNER_HEIGHT, vy }),
    );
    canvasInst.updateTailParticle();

    expectAfterParticleUpdate(spyTailUpdate, spyTailDraw, canvasInst.tailParticles, length);

    // Tail의 opacity가 기준값을 초과하면 (활성화 배열에서 아직 제거되지 않으면)
    if (length > 0) {
        // 투명도를 검사하는 메서드의 반환값은 false를 반환하고
        expect(canvasInst.tailParticles[0].belowOpacityLimit(TAIL.OPACITY_LIMIT)).toBeFalsy();
        
        // Text, Circle 파티클을 생성하는 메서드는 호출되지 않고
        expect(spyCreateTextParticle).not.toHaveBeenCalled();
        expect(spyCreateCircleParticle).not.toHaveBeenCalled();
    } else {
        // Tail의 opacity가 기준값 이하면
        // Text, Circle 파티클을 생성하는 메서드가 호출되고
        expect(spyCreateTextParticle).toHaveBeenCalledTimes(1);
        expect(spyCreateCircleParticle).toHaveBeenCalledTimes(1);
    }
});

 

3. TextParticle 업데이트 테스트

  • Text의 업데이트 관련 메서드의 호출을 감시할 스파이 변수를 선언한다.
  • 테스트 실행 전 스파이를 등록하고 실행 후 호출기록은 초기화 된다.
describe("TextParticle 업데이트 테스트", () => {
    let spyTextUpdate;
    let spyTextDraw;

    // 매번 테스트를 진행하기 전 스파이 등록
    beforeEach(() => {
        spyTextUpdate = jest.spyOn(TextParticle.prototype, "update");
        spyTextDraw = jest.spyOn(TextParticle.prototype, "draw");
    });
    
    // 생략...
}

  • 테스트를 위한 TextParticle의 생성과 설정을 담당하는 공통 메서드를 만들어 사용한다.
// CanvasUpdateParticle.test.js
function setTextParticles(params = {}) {
    canvasInst.init();
    // 원활한 테스트를 위해 1개만 추가
    const { x = 10, y = 10, vx = 20, vy = 20 } = params;
    canvasInst.textParticles.push(new TextParticle({ ctx: canvasInst.ctx, isSmallScreen: canvasInst.isSmallScreen, x, y, vx, vy }));
}

  • TextParticle의 opacity가 상한선을 초과하고 캔버스 영역 안에 있을 때는 파티클 제거 조건에 모두 부합하지 않으므로 활성화된 파티클 배열에 그대로 남아있다. 
    업데이트된 파티클이 제거 조건 메서드에 false를 반환, 파티클을 풀에 반환하는 메서드 미호출 여부를 확인하도록 한다.
// CanvasUpdateParticle.test.js
test("updateTextParticle 테스트 | opacity 상한선 이하(X), 캔버스 영역 밖(X)", () => {
    // 테스트 환경 세팅
    setTextParticles();
    canvasInst.updateTextParticle();

    expectAfterParticleUpdate(spyTextUpdate, spyTextDraw, canvasInst.textParticles, 1);

    const updatedParticle = canvasInst.textParticles[0];
    
    // 업데이트된 파티클이 제거 조건에 대해 false를 반환하는지 검증
    expect(updatedParticle.belowOpacityLimit()).toBeFalsy();
    expect(canvasInst.isOutOfCanvasArea(updatedParticle.x, updatedParticle.y)).toBeFalsy();
});

  • TextParticle의 opacity가 상한선 이하면 파티클은 활성화 배열에서 제거되어 풀에 반납된다. 
// CanvasUpdateParticle.test.js
test("updateTextParticle 테스트 | opacity 상한선 이하(O), 캔버스 영역  밖(X)", () => {
    setTextParticles();
    canvasInst.textParticles[0].opacity = 0; 
    canvasInst.updateTextParticle();

    expectAfterParticleUpdate(spyTextUpdate, spyTextDraw, canvasInst.textParticles, 0);
});

  • 또한 TextParticle이 캔버스 영역 밖에 있어도 파티클이 활성화 배열에서 제거되어 풀에 반납된다.
// CanvasUpdateParticle.test.js
test.each([
    { params: { x: 0, vx: -10 }, notice: "x축 좌측 밖" },
    { params: { x: INNER_WIDTH }, notice: "x축 우측 밖" },
    { params: { y: 0, vy: -30 }, notice: "y축 상단 밖" },
    { params: { y: INNER_HEIGHT }, notice: "y축 하단 밖" },
])("updateTextParticle 테스트 | opacity 상한선 이하(X), 캔버스 영역 밖(O, 캔버스 $notice)", ({ params }) => {
    setTextParticles(params);
    canvasInst.updateTextParticle();

    expectAfterParticleUpdate(spyTextUpdate, spyTextDraw, canvasInst.textParticles, 0);
});

 

4. CircleParticle 업데이트 테스트

  • CircleParticle을 update 하고 draw 하는 메서드의 호출 여부를 감시하기 위해 스파이 변수를 전역에서 선언한다.
    매번 테스트 전에 스파이를 등록하고 테스트 후에는 호출기록이 초기화된다.
describe("CircleParticle 업데이트 테스트", () => {
    let spyCircleUpdate;
    let spyCircleDraw;

    // 테스트 전 스파이 등록
    beforeEach(() => {
        spyCircleUpdate = jest.spyOn(CircleParticle.prototype, "update");
        spyCircleDraw = jest.spyOn(CircleParticle.prototype, "draw");
    });

    // 생략...
}

  • 초기 테스트 환경은 동일하므로 메서드를 만들어 중복 요소를 제거한다.
// CanvasUpdateParticle.test.js
function setUpdateCircleParticles(params = {}) {
    // 캔버스 초기화하고 파티클 생성해서 배열에 추가
    canvasInst.init();
    // 원활한 테스트를 위해 1개만 추가
    const { x = 10, y = 10, vx = 20, vy = 20, opacity = 0.9 } = params;
    canvasInst.circleParticles.push(
        new CircleParticle({ ctx: canvasInst.ctx, isSmallScreen: canvasInst.isSmallScreen, x, y, vx, vy, opacity }),
    );
    
    // 업데이트 진행
    canvasInst.updateCircleParticle();
}

  • CircleParticle의 opacity가 상한선을 초과하고 캔버스 영역 안에 위치해 있으면 다음 업데이트가 가능한 조건이다.
    배열에서 제거되지 않으므로 업데이트된 후 상황이 예상값과 일치하는지 확인한다.
// CanvasUpdateParticle.test.js
test("updateCircleParticle 테스트 | opacity 상한선 이하(X), 캔버스 영역 밖(X)", () => {
    setUpdateCircleParticles();

    expectAfterParticleUpdate(spyCircleUpdate, spyCircleDraw, canvasInst.circleParticles, 1);

    // 업데이트 된 파티클 제거 조건이 모두 false를 반환하는지 검증
    const updatedParticle = canvasInst.circleParticles[0];
    expect(updatedParticle.belowOpacityLimit()).toBeFalsy();
    expect(canvasInst.isOutOfCanvasArea(updatedParticle.x, updatedParticle.y)).toBeFalsy();
});

  • CircleParticle의 opacity가 상한선 이하이거나 캔버스 영역 밖에 위치해 있으면 활성화된 파티클 배열에서 제거되어 풀에 반납된다. 이 과정이 오류 없이 실행되었는지 검증한다.
// CanvasUpdateParticle.test.js
test.each([
    { params: { opacity: 0 }, opacity: "O", canvasOut: "X", notice: "안" },
    { params: { x: 0, vx: -10 }, opacity: "X", canvasOut: "O", notice: "x축 좌측 밖" },
    { params: { x: INNER_WIDTH }, opacity: "X", canvasOut: "O", notice: "x축 우측 밖" },
    { params: { y: 0, vy: -30 }, opacity: "X", canvasOut: "O", notice: "y축 상단 밖" },
    { params: { y: INNER_HEIGHT }, opacity: "X", canvasOut: "O", notice: "y축 하단 밖" },
])("updateCircleParticle 테스트 | opacity 상한선 이하($opacity), 캔버스 영역 밖($canvasOut, 캔버스 $notice)", ({ params }) => {
    setUpdateCircleParticles(params);

    expectAfterParticleUpdate(spyCircleUpdate, spyCircleDraw, canvasInst.circleParticles, 0);
});

 

5. SparkParticle 업데이트 테스트

  • 앞에 테스트와 비슷하게 모든 테스트마다 update, draw 메서드가 실행되므로 호출을 감시할 스파이를 전역에서 선언한다.
    테스트 전 스파이를 등록하고 테스트 후 스파이의 호출 기록은 초기화된다.
// CanvasUpdateParticle.test.js
describe("SparkParticle 업데이트 테스트", () => {
    // 호출을 감시할 스파이
    let spySparkUpdate;
    let spySparkDraw;

    // 테스트 전 스파이 등록
    beforeEach(() => {
        spySparkUpdate = jest.spyOn(SparkParticle.prototype, "update");
        spySparkDraw = jest.spyOn(SparkParticle.prototype, "draw");
    });

    // 생략...
}

  • 테스트의 설정(캔버스 초기화, SparkParticle 생성과 업데이트)은 동일하니 중복 코드가 없도를 공통 함수를 생성해서 사용한다.
// CanvasUpdateParticle.test.js
function setSparkParticles(params = {}) {
    canvasInst.init();
    // 원활한 테스트를 위해 1개만 추가
    const { x = 10, y = 10, vx = 20, vy = 20, opacity = 0.9 } = params;
    canvasInst.sparkParticles.push(
        new SparkParticle({ ctx: canvasInst.ctx, isSmallScreen: canvasInst.isSmallScreen, x, y, vx, vy, opacity }),
    );
    canvasInst.updateSparkParticle(); // 생성한 spark 업데이트
}

  • SparkParticle의 opacity가 상한선 초과, 위치도 캔버스 영역 안이면 다음에 업데이트가 가능하므로 활성화 배열에서 제거되지 않는다. 업데이트 후 파티클의 상태와 메서드 호출 여부가 예상값과 일치하는지 검증한다.
// CanvasUpdateParticle.test.js
test("updateSparkParticle 테스트 | opacity 상한선 이하(X), 캔버스 영역 밖(X)", () => {
    setSparkParticles();

    expectAfterParticleUpdate(spySparkUpdate, spySparkDraw, canvasInst.sparkParticles, 1);

    // 업데이트 된 파티클의 제거 조건이 모두 false를 반환하는지 검증
    const updatedParticle = canvasInst.sparkParticles[0];
    expect(updatedParticle.belowOpacityLimit()).toBeFalsy();
    expect(canvasInst.isOutOfCanvasArea(updatedParticle.x, updatedParticle.y)).toBeFalsy();
});

  • SparkParticle의 opacity가 상한선 이하이거나 위치가 캔버스 영역 밖이면 파티클 제거조건에 속하므로 활성화된 파티클 배열에서 제거하여 풀에 반납한다. 업데이트 후 배열에서 제거되었는지, 풀에 반환했는지 과정을 검증한다.
// CanvasUpdateParticle.test.js
test.each([
    { params: { opacity: 0 }, opacity: "O", canvasOut: "X", notice: "안" },
    { params: { x: 0, vx: -10 }, opacity: "X", canvasOut: "O", notice: "x축 좌측 밖" },
    { params: { x: INNER_WIDTH }, opacity: "X", canvasOut: "O", notice: "x축 우측 밖" },
    { params: { y: 0, vy: -30 }, opacity: "X", canvasOut: "O", notice: "y축 상단 밖" },
    { params: { y: INNER_HEIGHT }, opacity: "X", canvasOut: "O", notice: "y축 하단 밖" },
])("updateTextParticle 테스트 | opacity 상한선 이하($opacity), 캔버스 영역 밖($canvasOut, 캔버스 $notice)", ({ params }) => {
    setSparkParticles(params);

    expectAfterParticleUpdate(spySparkUpdate, spySparkDraw, canvasInst.sparkParticles, 0);
});

 

테스트 결과

npx jest .__test__/canvas/CanvasUpdateParticle.test.js

 

 

Github Repo
 

GitHub - jinsk9268/text-fireworks

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

github.com