테스트코드도 코드다.

pengooseDev·2023년 8월 21일
0

테스트 코드가 제대로 검증하고있는가?

우선, 문제가 있는 코드를 살펴보자.

import { PRODUCTS } from '../../../src/constants';
import { LottoCorporation, Store } from '../../../src/Model';

describe('LottoCorporation', () => {
  const lottoCorporation = new LottoCorporation(new Store(PRODUCTS));

  test('로또회사가 인수한 상점에서 로또를 구매할 수 있다.', () => {
    const purchaseAmount = 5_000;
    const tickets = lottoCorporation.buyTickets(purchaseAmount);

    tickets.forEach((ticket) =>
      expect(ticket).toBeInstanceOf(PRODUCTS.LottoTicket.product)
    );
  });
});

해당 코드를 작성한 시점을 기준으로 설명하자면 Store는 판매할 Product(LottoTicket Class)를 DI받아 가격에 맞는 수량을 판매한다.

당시엔 도메인의 역할에 맞게, 구매한 로또 Ticket(Product)의 instance 여부만 확인만 하면 되지않나?라는 생각으로 작성한 코드이다.

하지만 다시 살펴보니 해당 로직은 "Store" 자체의 로직이고 이를 DI 받아 판매하는 LottoCorporation 도메인은 해당 Ticket이 정상적인 LottoTicket인지 LottoTicket에서 진행한 일련의 테스트를 동일하게 진행해야 했다.


단위 분리.

우선 해당 test 내에 필요한 검증요소를 정리했다.

import { LottoCorporation } from '../../../src/Model';
import { NUMBER } from '../../../src/constants';

describe('LottoCorporation', () => {
  const lottoCorporation = new LottoCorporation();

  test('로또회사가 인수한 상점에서 로또를 구매할 수 있다.', () => {
    const purchaseAmount = 5_000;
    const tickets = lottoCorporation.buyTickets(purchaseAmount);
    const TICKET_PRICE = NUMBER.DEFAULT_TICKET_PRICE;
    const expectedTicketCount = purchaseAmount / TICKET_PRICE;

    // 로또 티켓의 개수가 올바른지 검증
    expect(tickets.length).toBe(expectedTicketCount);

    // 각 로또 티켓에 대한 검증
    tickets.forEach(ticket => {
      const numbers = ticket.getTicketNumbers();

      // 숫자들이 올바른 범위에 있는지 검증
      numbers.forEach(num => {
        expect(num).toBeGreaterThanOrEqual(NUMBER.LOTTO_TICKET.MIN_RANGE);
        expect(num).toBeLessThanOrEqual(NUMBER.LOTTO_TICKET.MAX_RANGE);
      });

      // 중복된 숫자가 없는지 검증
      const uniqueNumbers = new Set(numbers);
      expect(uniqueNumbers.size).toBe(numbers.length);

      // 숫자들이 정렬되어 있는지 검증
      for (let i = 0; i < numbers.length - 1; i++) {
        expect(numbers[i]).toBeLessThanOrEqual(numbers[i + 1]);
      }
    });
  });
});

얼핏 보아도 알겠지만, 하나의 테스트코드에 너무 많은 검증이 들어가있어 분리하는 것이 좋아보여 테스트를 분리하기 시작했다.


테스트코드도 코드다

테스트 코드 또한 리팩터링의 대상이라는 의미를 담고있다.
코드가 성장하며 관리되는 것처럼, 테스트 코드 또한 담당하고 있는 테스트의 역할이 과도하지 않은지 고민해보자.

각 LottoTicket이 가져야하는 조건들을 기준으로 테스트를 분리하고 코드를 작성했다.

import { LottoCorporation } from '../../../src/Model';
import { NUMBER } from '../../../src/constants';
import { isLottoNumber } from '../../../src/utils/Validator';

describe('LottoCorporation', () => {
  const lottoCorporation = new LottoCorporation();

  const purchaseAmount = 5_000;
  const tickets = lottoCorporation.buyTickets(purchaseAmount);

  test('구매금액에 맞는 로또의 개수를 반환하는지 확인한다.', () => {
    const TICKET_PRICE = NUMBER.DEFAULT_TICKET_PRICE;
    const expectedTicketCount = parseInt(purchaseAmount / TICKET_PRICE);

    expect(tickets.length).toBe(expectedTicketCount);
  });

  test.each(tickets)(
    '발급된 로또의 숫자 범위가 유효한지 확인한다.',
    (ticket) => {
      const numbers = ticket.getTicketNumbers();
      const numberValidation = numbers.every(isLottoNumber);

      expect(numberValidation).toBeTruthy();
    }
  );

  test.each(tickets)(
    '발급된 로또엔 중복된 숫자가 존재하지 않는다.',
    (ticket) => {
      const ticketNumbers = ticket.getTicketNumbers();
      const uniqueNumbers = new Set(ticketNumbers);

      expect(uniqueNumbers.size === ticketNumbers.length).toBeTruthy();
    }
  );

  test.each(tickets)(
    '발급된 로또의 숫자들은 오름차순으로 정렬되어있다.',
    (ticket) => {
      const ticketNumbers = ticket.getTicketNumbers();
      const sortedTicketNumbers = [...ticketNumbers].sort((a, b) => a - b);

      for (let i = 0; i < ticketNumbers.length; i++) {
        expect(ticketNumbers[i] === sortedTicketNumbers[i]).toBeTruthy();
      }
    }
  );

  const mockCases = [
    {
      ticketNumbers: [1, 2, 3, 4, 5, 6],
      winningNumbers: { lottoNumbers: [1, 2, 3, 4, 5, 6], bonusNumber: 7 },
      expectedValue: 6,
    },
    {
      ticketNumbers: [11, 12, 13, 14, 15, 16],
      winningNumbers: { lottoNumbers: [1, 2, 3, 4, 5, 6], bonusNumber: 7 },
      expectedValue: 0,
    },
  ];

  test.each(mockCases)(
    '로또 티켓의 결과를 확인한다.',
    ({ ticketNumbers, winningNumbers, expectedValue }) => {
      const { matchingCount } = lottoCorporation.checkTicketResult(
        ticketNumbers,
        winningNumbers
      );

      expect(matchingCount).toBe(expectedValue);
    }
  );
});

0개의 댓글