[Refac] 보드게임 BLOKUS 리팩터링하기 - 3.5

este·2024년 11월 3일
0

BLOKUS-SvelteKit

목록 보기
5/8

블록이 유효한지 확인하는 로직에 대한 테스트를 작성해보자

테스트를 작성하려면 테스트 환경이 잘 구성되어야 한다. 우선 환경을 구성한 후 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()

블록이 보드 범위 내에 있는지 확인하려면 아래 네 가지 케이스를 작성하면 될 것 같다.

  1. 블록이 보드 내에 들어가면 true 반환
    1-1. 블록이 보드 중앙에 위치하면 true 반환
    1-2. 블록이 보드 경계에 위치하면 true 반환
  2. 블록이 범위를 넘어가면 false 반환
    2-1. 블록이 보드의 상하 범위를 초과하면 false 반환
    2-2. 블록이 보드의 좌우 범위를 초과하면 false 반환

이를 위해 작은 기역자([[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. 첫 턴인 경우 모서리에 놓으면 true 반환
  2. 첫 턴인 경우 모서리에 놓지 않으면 false 반환
  3. 첫 턴이 아닌 경우 false 반환

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()

자신의 다른 블록과 대각 연결이 존재하는지 확인하는 케이스는 이렇게 나눌 수 있을 것 같다.

  1. 연결이 유효하지 않은 경우
    1-1. 놓으려는 블록 주위에 아무런 블록도 없는 경우
    1-2. 놓으려는 블록 주위에 다른 플레이어의 블록이 있는 경우
  2. 연결이 유효한 경우
    2-1. 좌상단 연결이 유효한 경우
    2-2. 우상단 연결이 유효한 경우
    2-3. 우하단 연결이 유효한 경우
    2-4. 좌하단 연결이 유효한 경우
  3. 심화 테스트

연결이 유효하지 않은 경우부터 작성해보자.

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를 곁들여보려고 한다. 구현할 기능에 대한 문서 및 정의가 테스트로 잘 정의되었으면 하는 바램이다.

profile
este / 에스테입니다.

0개의 댓글