이전 글 : [Canvas-불꽃놀이-19] CanvasOption 단위 테스트
Particle 클래스 단위 테스트 진행
Particle 클래스 단위 테스트
Particle.js 전체 코드 확인
더보기
import { randomFloat, setRgbaColor } from "@/js/utils.js";
import { PARTICLE } from "@/js/constants.js";
class Particle {
/**
* 모든 파티클의 부모 클래스
* @param {object} params
* @param {CanvasRenderingContext2D} params.ctx
* @param {boolean} params.isSmallScreen
* @param {number} [params.x]
* @param {number} [params.y]
* @param {number} [params.vx]
* @param {number} [params.vy]
* @param {number} [params.radius]
* @param {number} [params.opacity]
* @param {number} [params.friction]
* @param {number} [params.color]
*/
constructor({ ctx, isSmallScreen, x, y, vx, vy, radius, opacity, friction, color }) {
this.ctx = ctx;
this.isSmallScreen = isSmallScreen;
this.initParticleVars({ x, y, vx, vy, radius, opacity, friction, color });
this.initialState = {};
}
initParticleVars(params = {}) {
const {
x = 0,
y = 0,
vx = 0,
vy = 0,
radius = PARTICLE.RADIUS,
opacity = randomFloat(PARTICLE.MIN_OPACITY, PARTICLE.MAX_OPACITY),
friction = PARTICLE.FRICTION,
color = setRgbaColor(PARTICLE.RGB, opacity),
} = params;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.radius = Math.max(radius, 0) * (this.isSmallScreen ? PARTICLE.RADIUS_ADJUST_RATIO : 1);
this.opacity = opacity;
this.friction = friction;
this.fillColor = color;
}
draw() {
this.ctx.beginPath();
this.ctx.fillStyle = this.fillColor;
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.closePath();
}
updateVelocity() {
this.vx *= this.friction;
this.vy *= this.friction;
}
updatePosition() {
this.x += this.vx;
this.y += this.vy;
}
update() {
this.updateVelocity();
this.updatePosition();
}
/**
* 파티클 공통 멤버변수 초기화 및 재사용을 위한 리셋
* @param {object} [params]
*/
reset(params = {}) {
for (const key in this.initialState) params[key] = this.initialState[key];
this.initParticleVars(params);
}
/**
* @param {number} [opacityLimit]
* @returns 파티클의 투명도가 제한 기준 이하면 true를 반환, 초과면 false를 반환
*/
belowOpacityLimit(opacityLimit = 0) {
return this.opacity <= opacityLimit;
}
}
export default Particle;
모든 하위 파티클에 대한 부모 클래스인 Particle.js의 단위 테스트를 진행하도록 한다.
1. 공통 설정
생성자에 사용되는 메서드 mock 처리
- Particle 클래스 멤버변수 기본값 설정 시 randomInt, setRgbaColor 메서드를 사용하는 변수가 있다. 이 메서드들은 이전에 검증되었으므로 mock 함수 처리를 해준다.
- (추가) 멤버변수 기본값 설정 시 사용된 함수의 호출여부 검증을 위해 utils.js에서 검증에 사용할 실사용 함수를 사용할 수 있게 한다.
// utils.js
/**
* @param {number | string | boolean | undefined} value
* @returns 파라미터 값이 undefined면 true 반환, 아니면 false 반환
*/
export function isUndefined(value) {
return value === undefined;
}
// Particle.test.js
jest.mock("@/js/utils", () => {
// isUndefined 실사용 가능하도록 설정
const { isUndefined } = jest.requireActual("@/js/utils");
// isUndefined 실사용 | randomFloat, setRgbaColor mock 함수화
return {
isUndefined,
randomFloat: jest.fn(),
setRgbaColor: jest.fn(),
};
});
공통 상수 추가
- constants.js 인자 전달을 하지 않을 시 default 값으로 설정되는 객체를 상수로 추가한다.
// constants.js
const DEFAULT_PARTICLE_COLOR = setRgbaColor(PARTICLE.RGB, PARTICLE.MIN_OPACITY);
export const TEST_OPTION = {
// 생략...
// Particle 멤버변수 기본값
PARTICLE_DEFAULT_VALUES: {
x: 0,
y: 0,
vx: 0,
vy: 0,
radius: PARTICLE.RADIUS,
opacity: PARTICLE.MIN_OPACITY,
friction: PARTICLE.FRICTION,
color: DEFAULT_PARTICLE_COLOR,
},
}
setup.js에 반복되는 로직 분리
- jest-canvas-mock을 사용한 ctx 생성도 공통적으로 들어가는 부분이니 분리하도록 한다.
- 파티클의 전체 멤버 변수의 값을 확인하는 일이 많아 이 부분을 분리하여 중복을 제거하도록 한다.
// setup.js
/**
* @returns {CanvasRenderingContext2D} jest-canvas-mock의 ctx 반환
*/
export const createMockCanvasCtx = () => {
const canvas = document.createElement("canvas");
return canvas.getContext("2d");
};
/**
* 파티클의 멤버 변수값이 예측 결과값과 일치하는지 검증
* @param {Particle} particle
* @param {object} expectedResult
*/
function expectAllParticleVars(particle, expectedResult) {
expect(particle.x).toBe(expectedResult.x);
expect(particle.y).toBe(expectedResult.y);
expect(particle.vx).toBe(expectedResult.vx);
expect(particle.vy).toBe(expectedResult.vy);
expect(particle.radius).toBe(expectedResult.radius);
expect(particle.opacity).toBe(expectedResult.opacity);
expect(particle.friction).toBe(expectedResult.friction);
expect(particle.fillColor).toBe(expectedResult.color);
}
테스트 공통 설정 추가
- Particle 생성시 꼭 필요한 ctx, isSmallScreen은 전역적으로 사용하도록 한다.
- mock처리한 randomInt, setRgbaColor은 기본값을 반환하도록 설정한다. 값이 변경될 여지는 없으므로 beforeAll에서 설정한다.
- ctx, isSmallScreen은 새로운 테스트를 진행할 때마다 초기화되어 똑같은 조건에서 테스트를 진행할 수 있도록 beforeEach에서 설정한다.
// Particle.test.js
describe("Particle 클래스 테스트", () => {
// 전역 사용 변수
let ctx;
let isSmallScreen;
const { PARTICLE_DEFAULT_VALUES } = TEST_OPTION;
// 한번만 설정하고 테스트 진행시 처음 설정된 값으로 진행된다.
beforeAll(() => {
// 기본값 설정에 쓰이는 메서드 지정한 반환값 반환하도록 설정
randomFloat.mockReturnValue(PARTICLE_DEFAULT_VALUES.opacity);
setRgbaColor.mockReturnValue(PARTICLE_DEFAULT_VALUES.color);
});
// 매번 테스트 진행 전 새롭게 생성된다.
beforeEach(() => {
// jest-canvas-mock 사용
ctx = ctx = createMockCanvasCtx();
// 기본적으로 false로 설정하고 필요시 테스트 내에서 true로 변경
isSmallScreen = false;
});
// 테스트 후 등록한 스파이의 호출기록 일괄적으로 초기화
afterEach(() => {
jest.clearAllMocks();
});
// 생략...
})
2. Particle 생성자와 멤버변수 초기화 테스트
- 파티클의 멤버변수가 전달된 값으로 설정되는지 확인한다.
- 스크린 크기, 기본값, 기본값 + 커스텀값, 커스텀값으로 구성된 조합으로 무작위성을 검증한다.
- PC 화면, 기본값, 커스텀값, 기본값 + 커스텀값으로 구성된 조합으로 무작위성을 검증한다.
- (추가) 값뿐만 아니라 멤버변수 기본값 설정 시 사용된 함수의 호출 여부도 확인해 검증 과정을 강화한다.
// Particle.test.js
test.each([
{ params: {}, notice: "기본값" },
{
params: { x: 20, y: 20, radius: 5, opacity: 0.8, friction: 0.95, color: "rgba(255, 255, 255, 1)" },
notice: "커스텀값",
},
{ params: { x: 30, y: 30, vx: 2, vy: 2, radius: 4 }, notice: "기본값 + 커스텀값" },
])("Particle 생성자와 멤버변수 초기화 (PC화면 | $notice)", ({ params }) => {
const particle = new Particle({ ctx, isSmallScreen, ...params });
const expectedResult = { ...PARTICLE_DEFAULT_VALUES, ...params };
expectAllParticleVars(particle, expectedResult);
// 추가 (기본값 설정 시 사용된 함수 호출여부 확인)
if (isUndefined(params.opacity)) expect(randomFloat).toHaveBeenCalled();
if (isUndefined(params.color)) expect(setRgbaColor).toHaveBeenCalled();
});
- 모바일 화면, 기본값 + 커스텀 값으로 구성된 조합을 검증하도록 한다.
위에서 모든 경우가 검증되었기 때문에 기본값+커스텀값만 검증하도록 한다.
// Particle.test.js
test("Particle 생성자와 멤버변수 초기화 (모바일 화면 | 기본값 + 커스텀값)", () => {
const params = { isSmallScreen: true, x: 10, y: 10, radius: 2, opacity: 0.5, color: "hsla(60, 100%, 100%)" };
const particle = new Particle({ ctx, isSmallScreen, ...params });
const expectedResult = { ...PARTICLE_DEFAULT_VALUES, ...params };
expectedResult.radius *= PARTICLE.RADIUS_ADJUST_RATIO;
expectAllParticleVars(particle, expectedResult);
});
- 테스트 시 반지름에 음수를 넣는 경우도 생길 수 있다는 걸 인지하게 됐다. ctx.arc 메서드를 실행할 때 반지름이 음수이면 캔버스에 그리지 않는다. 좀 더 찾아보니 반지름이 음수인 경우는 존재하지 않으므로 캔버스에 똑같이 그리진 않지만 논리적으로 오류가 없는 0으로 바꿔주는 것이 좋다.
// Particle.js
initParticleVars(params = {}) {
// 생략...
// radius가 음수일 시 0을 적용한다.
this.radius = Math.max(radius, 0) * (this.isSmallScreen ? PARTICLE.RADIUS_ADJUST_RATIO : 1);
}
// Particle.test.js
test("Particle 생성시 반지름이 음수일 때", () => {
const particle = new Particle({ ctx, isSmallScreen, radius: -2 });
expect(particle.radius).toBe(0);
});
3. draw 테스트
- 생성한대로 메서드가 호출되는지, 중복 호출은 없는지, 인자로 전달한 값으로 잘 생성되는지 검증한다.
// Particle.test.js
test("Particle draw 테스트", () => {
const [x, y, radius, color] = [100, 100, 10, "#171717"];
const particle = new Particle({ ctx, isSmallScreen, x, y, radius, color });
particle.draw();
// ctx.beginPath가 실행되고 1번만 호출되었는지 확인
expect(particle.ctx.beginPath).toHaveBeenCalledTimes(1);
expect(particle.ctx.fillStyle).toBe(particle.fillColor);
// ctx.arc가 실행되고 1번만 호출되었는지 확인
expect(particle.ctx.arc).toHaveBeenCalledWith(x, y, radius, 0, Math.PI * 2);
expect(particle.ctx.arc).toHaveBeenCalledTimes(1);
// ctx.fill이 실행되고 1번만 호출되었는지 확인
expect(particle.ctx.fill).toHaveBeenCalledTimes(1);
// ctx.close가 실행되고 1번만 호출되었는지 확인
expect(particle.ctx.closePath).toHaveBeenCalledTimes(1);
});
4. update 테스트
- update 메서드는 속도 업데이트와 좌표 업데이트로 이루어져 있으므로 이 둘을 먼저 검증한다.
// Particle.test.js
// 속도
test("Particle update 테스트 (속도)", () => {
const [vx, vy, friction] = [10, 10, 0.9];
const particle = new Particle({ ctx, isSmallScreen, vx, vy, friction });
particle.updateVelocity();
expect(particle.vx).toBe(vx * friction);
expect(particle.vy).toBe(vy * friction);
});
// 위치
test("Particle update 테스트 (위치)", () => {
const [x, y, vx, vy] = [100, 200, 10, 20];
const particle = new Particle({ ctx, isSmallScreen, x, y, vx, vy });
particle.updatePosition();
expect(particle.x).toBe(x + vx);
expect(particle.y).toBe(y + vy);
});
- update 메서드는 위에서 검증한 updateVelocity, updatePosition을 실행하는 통합 메서드이다.
// Particle.test.js
test("Particle update 테스트 (통합)", () => {
const particle = new Particle({ ctx, isSmallScreen, x: 1, y: 1, vx: 10, vy: 10 });
// updateVelocity, updatePosition이 호출되는지 감시하기위해 jest spy로 등록
let spyUpdateVelocity = jest.spyOn(particle, "updateVelocity");
let spyUpdatePostion = jest.spyOn(particle, "updatePosition");
// 업데이트 실행
particle.update();
// update 메서드를 구성하고있는 updateVelocity, updatePosition이 한번씩 호출되는지 검증
expect(spyUpdateVelocity).toHaveBeenCalledTimes(1);
expect(spyUpdatePostion).toHaveBeenCalledTimes(1);
});
5. reset 테스트
- Particle의 reset 메서드는 두 가지 역할을 수행한다.
1. 사용된 파티클의 속성들을 기본값과 파티클 고윳값(initialState)으로 초기화하여 풀에 반납하는 역할을 수행
2. 초기화된 파티클을 풀에서 가져와 전달된 값으로 파티클의 속성을 초기화하는 역할을 수행 - 이에 각 역할에 맞는 테스트를 생성해서 검증하도록 한다.
- 사용된 파티클을 풀에 반납할 때 initialState이 유무를 검증한다.
// Particle.test.js
test.each([
{ initialState: {}, notice: "initialState 없음" },
{ initialState: { vx: 5, vy: 5 }, notice: "initialState 있음" },
])("Particle reset 테스트 (사용된 파티클 풀에 리턴시 초기화 | $notice)", ({ initialState }) => {
// 사용된 파티클을 초기화 하는것이니 인자를 전달해 테스트 파티클을 생성하도록 하고 initialState도 지정한다.
const particle = new Particle({ ctx, isSmallScreen, x: 10, y: 10, vx: 1, vy: 1 });
particle.initialState = initialState;
// 리셋을 진행하고 예상 결과를 생성한다.
particle.reset();
const expectedResult = { ...PARTICLE_DEFAULT_VALUES, ...initialState };
// 파티클 값과 예상 결과값이 일치하는지 확인한다.
expectAllParticleVars(particle, expectedResult);
});
- 풀에서 파티클을 가져와 전달받은 파라미터 값으로 파티클의 속성값을 지정한다.
// Particle.test.js
test.each([
{
params: { x: 5, y: 5, vx: 2, vy: 2, radius: 0.8, opacity: 0.5, friction: 0.6, color: "rgba(0, 0, 0, 1)" },
initialState: {},
notice: "initialState 없음",
},
{
params: { x: 10, y: 10, vx: 20, vy: 20, radius: 2, opacity: 0.9 },
initialState: { friction: 0.6, color: "rgba(0, 0, 0, 1)" },
notice: "initialState 있음",
},
])("Particle reset 테스트 (풀에서 꺼내와서 재사용 | $notice)", ({ params, initialState }) => {
// 초기화된 파티클을 생성해서 테스트 진행
const particle = new Particle({ ctx, isSmallScreen });
particle.initialState = initialState;
// 재사용을 위해 인자값을 전달해서 파티클의 속성을 초기화하도록 한다.
particle.reset(params);
// 예측 결과값 (기본값 객체을 먼저 베이스로 두고, 인자로 전달된 객체, initialState로 덮어야 함)
const expectedResult = { ...PARTICLE_DEFAULT_VALUES, ...params, ...initialState };
// 재사용 파티클의 속성값과 예측 결과를 검증한다.
expectAllParticleVars(particle, expectedResult);
});
6. belowOpacityLimit 테스트
- 투명도 제한도 다양한 케이스를 통해 무작위성을 검증한다. (undefined는 기본값 적용)
// Particle.test.js
test.each([
{ opacity: 1, opacityLimit: 0.5, expectedResult: false, notice: "커스텀 적용" },
{ opacity: 0.1, opacityLimit: 0.5, expectedResult: true, notice: "커스텀 적용" },
{ opacity: -0.004, opacityLimit: 0.1, expectedResult: true, notice: "커스텀 적용" },
{ opacity: 0.5, opacityLimit: undefined, expectedResult: false, notice: "기본값 0 적용" },
{ opacity: -0.01, opacityLimit: undefined, expectedResult: true, notice: "기본값 0 적용" },
])("Particle 투명도 제한 기준 테스트: $opacity < $opacityLimit (opacityLimit $notice)", ({ opacity, opacityLimit, expectedResult }) => {
const particle = new Particle({ ctx, isSmallScreen, opacity });
expect(particle.belowOpacityLimit(opacityLimit)).toBe(expectedResult);
});
테스트 결과
npx jest ./__test__/particle/Particle.test.js
Github Repo
'Canvas > 불꽃놀이 프로젝트' 카테고리의 다른 글
[Canvas-불꽃놀이-22] TextParticle 클래스 단위 테스트 (1) | 2024.12.08 |
---|---|
[Canvas-불꽃놀이-21] TailParticle 클래스 단위 테스트 (1) | 2024.12.07 |
[Canvas-불꽃놀이-19] CanvasOption 단위 테스트 (2) | 2024.12.05 |
[Canvas-불꽃놀이-18] utils.js 단위 테스트 (0) | 2024.12.04 |
[Canvas-불꽃놀이-17] Jest 설정과 테스트 진행 순서 (1) | 2024.12.03 |