블록이 유효한지 확인하는 로직에 대한 테스트를 작성해보자
테스트를 작성하려면 테스트 환경이 잘 구성되어야 한다. 우선 환경을 구성한 후 jest를 사용해서 isBlockPlaceable
내부의 함수들을 테스트하는 코드를 작성해보자.
기존에 레디스 커넥션 풀을 구성하기 위해 jest.config.json을 아래와 같이 작성했다.
{
"testPathIgnorePatterns": [
"/node_modules/",
"/dist/",
"/.js$/"
],
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
}
}
여기에 SvelteKit에서 사용하는 alias인 $를 사용하도록 moduleNameMapper
속성을 추가해보겠다.
"moduleNameMapper": {
"^\\$lib/(.*)$": "<rootDir>/src/lib/$1"
}
이제 game.test.ts
를 작성해보자. 보드를 매번 초기화해야 하니 보드를 생성하는 함수를 내부에 선언해주겠다.
describe('isBlockPlaceable 내부 로직 검사', () => {
const createEmptyBoard = (): BoardMatrix =>
Array(20).fill(undefined).map(() => Array(20).fill(false));
...
블록은 상황에 따라 만들어 쓰도록 하겠다.
isWithinBoardBounds()
블록이 보드 범위 내에 있는지 확인하려면 아래 네 가지 케이스를 작성하면 될 것 같다.
이를 위해 작은 기역자([[true, true], [true, false]]
, type 31) 블록을 사용해보겠다. 코드로 작성하면 아래와 같이 되겠다.
describe('isWithinBoardBounds', () => {
const block: BlockMatrix = getBlockMatrix({ type: '31', rotation: 0, flip: false });
describe('블록이 보드 경계를 넘지 않는 경우', () => {
test('작은 기역자 블록을 중앙에 배치하면 보드 내부에 완전히 들어가 true를 반환', () => {
// given
const board = createEmptyBoard();
const dto: PlaceBlockDTO = { block, position: [9, 9], board, playerIdx: 0, turn: 0 };
// when
const result = isWithinBoardBounds(dto);
// then. 이하 주석 생략
expect(result).toBe(true);
});
test('작은 기역자 블록을 보드 경계에 배치하면 보드 내부에 완전히 들어가 true를 반환', () => {
const board = createEmptyBoard();
[[0, 0], [0, 18], [18, 18], [18, 0]].forEach((position) => {
const dto: PlaceBlockDTO = { block, position, board, playerIdx: 0, turn: 0 };
const result = isWithinBoardBounds(dto);
expect(result).toBe(true);
});
});
});
describe('블록이 보드 경계를 넘어가는 경우', () => {
test('블록이 보드 상하 경계를 넘으면 false를 반환', () => {
const board = createEmptyBoard();
const upwardTestDTO: PlaceBlockDTO = { block, position: [-1, 0], board, playerIdx: 0, turn: 0 }
const downwardTestDTO: PlaceBlockDTO = { block, position: [19, 0], board, playerIdx: 0, turn: 0 }
const upwardResult = isWithinBoardBounds(upwardTestDTO);
const downwardResult = isWithinBoardBounds(downwardTestDTO);
expect(upwardResult).toBe(false);
expect(downwardResult).toBe(false);
});
test('블록이 보드 좌우 경계를 넘으면 false를 반환', () => {
const board = createEmptyBoard();
const leftwardTestDTO: PlaceBlockDTO = { block, position: [0, -1], board, playerIdx: 0, turn: 0 }
const rightwardTestDTO: PlaceBlockDTO = { block, position: [0, 19], board, playerIdx: 0, turn: 0 }
const leftwardResult = isWithinBoardBounds(leftwardTestDTO);
const rightwardResult = isWithinBoardBounds(rightwardTestDTO);
expect(leftwardResult).toBe(false);
expect(rightwardResult).toBe(false);
});
});
});
isFirstMoveValid()
각 플레이어가 첫 턴일 때 모서리에 놓았는지 확인하려면 크게 세 가지 케이스로 나눠서 보면 될 것 같다.
1~2는 하나로 묶어 아래와 같이 케이스를 작성하면 될 것 같다.
describe('isFirstMoveValid', () => {
const singleCellBlock: BlockMatrix = getBlockMatrix({ type: '10', rotation: 0, flip: false });
const board = createEmptyBoard();
describe('첫 턴인 경우', () => {
test('코너에 블록을 놓으면 true를 반환', () => {
const cornerPositions = [[0, 0], [0, 19], [19, 19], [19, 0]];
cornerPositions.forEach((position, idx) => {
const dto: PlaceBlockDTO = {
block: singleCellBlock,
board,
position,
playerIdx: idx as 0 | 1 | 2 | 3,
turn: idx,
};
const result = isFirstMoveValid(dto);
expect(result).toBe(true);
});
});
test('코너가 아닌 곳에 블록을 놓으면 false를 반환', () => {
const dto: PlaceBlockDTO = {
block: singleCellBlock,
position: [1, 1],
board,
playerIdx: 0,
turn: 0,
};
const result = isFirstMoveValid(dto);
expect(result).toBe(false);
});
});
test('첫 턴이 아닐 때는 코너가 아니어도 true를 반환', () => {
const dto: PlaceBlockDTO = {
block: singleCellBlock,
position: [1, 1],
board,
playerIdx: 0,
turn: 4,
};
const result = isFirstMoveValid(dto);
expect(result).toBe(true);
});
});
hasDiagonalConnection()
자신의 다른 블록과 대각 연결이 존재하는지 확인하는 케이스는 이렇게 나눌 수 있을 것 같다.
연결이 유효하지 않은 경우부터 작성해보자.
describe('hasDiagonalConnection', () => {
const singleCellBlock: BlockMatrix = getBlockMatrix({ type: '10', rotation: 0, flip: false });
const block: BlockMatrix = getBlockMatrix({ type: '54', rotation: 0, flip: false })
let board = createEmptyBoard();
beforeEach(() => board = createEmptyBoard());
describe('대각 연결이 유효하지 않는 경우', () => {
test('주변에 블록이 없는 경우 false를 반환', () => {
const dto: PlaceBlockDTO = {
block: block,
position: [5, 5],
board,
playerIdx: 0,
turn: 4,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(false);
});
test('다른 플레이어의 블록과 대각선으로 연결된 경우 false를 반환', () => {
placeBlock({ board, block: singleCellBlock, playerIdx: 1, position: [0, 0], turn: 0 })
const dto: PlaceBlockDTO = {
block: singleCellBlock,
position: [1, 1],
board,
playerIdx: 0,
turn: 1,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(false);
});
test('대각선이 아닌 방향으로만 블록이 있는 경우 false를 반환', () => {
placeBlock({ block: singleCellBlock, position: [0, 1], board, playerIdx: 0, turn: 0 })
placeBlock({ block: singleCellBlock, position: [1, 0], board, playerIdx: 0, turn: 0 })
const dto: PlaceBlockDTO = {
block: singleCellBlock,
position: [0, 0],
board,
playerIdx: 0,
turn: 0,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(false);
});
});
...
beforeEach
는 중첩된 describe
내부 test
함수 호출 시에도 실행되니 이와 같이 한 번만 작성해도 되겠다. 다음은 연결이 유효한 경우의 테스트 케이스를 작성해보겠다.
...
describe('대각 연결이 유효한 경우', () => {
test('좌상단 연결이 유효한 경우 true를 반환', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [0, 0],
turn: 0
});
const dto: PlaceBlockDTO = {
block: singleCellBlock,
board,
playerIdx: 0,
position: [1, 1],
turn: 1,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('우상단 연결이 유효한 경우 true를 반환', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [0, 19],
turn: 0
});
const dto: PlaceBlockDTO = {
block: singleCellBlock,
board,
playerIdx: 0,
position: [1, 18],
turn: 1,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('우하단 연결이 유효한 경우 true를 반환', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [19, 19],
turn: 0,
});
const dto: PlaceBlockDTO = {
block: singleCellBlock,
board,
playerIdx: 0,
position: [18, 18],
turn: 1,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('좌하단 연결이 유효한 경우 true를 반환', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [19, 0],
turn: 0
});
const dto: PlaceBlockDTO = {
block: singleCellBlock,
board,
playerIdx: 0,
position: [18, 1],
turn: 1,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
});
...
좌상단부터 시작하여 시계방향으로 연결이 유효한지 확인하는 케이스를 작성했다. 이제 복잡한 모양인 type 54, [[true, false, false], [true, true, true], [false, true, false]]
블록의 모든 대각선을 체크하는 케이스도 작성해보겠다.
...
describe('복잡한 모양(type 54)의 블록 테스트', () => {
const block = getBlockMatrix({
type: '54',
rotation: 0,
flip: false,
});
test('(-1, -1)', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [0, 0],
turn: 0,
});
const dto: PlaceBlockDTO = {
block,
position: [1, 1],
board,
playerIdx: 0,
turn: 0,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('(1, -1)', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [0, 1],
turn: 0,
});
const dto: PlaceBlockDTO = {
block,
position: [1, 0],
board,
playerIdx: 0,
turn: 0,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('(2, -1)', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [2, 0],
turn: 0,
});
const dto: PlaceBlockDTO = {
block,
position: [0, 1],
board,
playerIdx: 0,
turn: 0,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('(3, 0)', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [3, 0],
turn: 0,
});
const dto: PlaceBlockDTO = {
block,
position: [0, 0],
board,
playerIdx: 0,
turn: 0,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('(3, 2)', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [3, 2],
turn: 0,
});
const dto: PlaceBlockDTO = {
block,
position: [0, 0],
board,
playerIdx: 0,
turn: 0,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('(2, 3)', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [2, 3],
turn: 0,
});
const dto: PlaceBlockDTO = {
block,
position: [0, 0],
board,
playerIdx: 0,
turn: 0,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('(0, 3)', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [0, 3],
turn: 0,
});
const dto: PlaceBlockDTO = {
block,
position: [0, 0],
board,
playerIdx: 0,
turn: 0,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
test('(-1, 1)', () => {
placeBlock({
block: singleCellBlock,
board,
playerIdx: 0,
position: [0, 1],
turn: 0,
});
const dto: PlaceBlockDTO = {
block,
position: [1, 0],
board,
playerIdx: 0,
turn: 0,
};
const result = hasDiagonalConnection(dto);
expect(result).toBe(true);
});
});
여지껏 구현한 로직들에 대한 테스트는 다 작성된 것 같다.
다음에 구현할 기능부터는 TDD를 곁들여보려고 한다. 구현할 기능에 대한 문서 및 정의가 테스트로 잘 정의되었으면 하는 바램이다.