NEXT-STEP 2주차 미션(자동차 경주 - step2)

jiny·2023년 8월 4일
0
post-thumbnail

2주차 미션을 PR 올린 시점에서 2주차에서 시도해보고 새로운 레퍼런스를 얻었던 내용 들에 대해 회고해보려고 한다.

테스팅

each 메서드 활용

// Car.test.js
describe('자동차 게임 우승자 출력 테스트', () => {
  test('자동차 게임이 완료되었을 때 우승자는 최소 1명 이상 나올 수 있다.', () => {
    const racingWinners = new RacingWinners();
    racingWinners.setRacingWinners([
      'jiny : -\nmouse : -',
      'jiny : -\nmouse : --',
      'jiny : --\nmouse : ---',
      'jiny : ---\nmouse : ----',
      'jiny : -----\nmouse : -----',
    ]);
    const winners = racingWinners.getRacingWinners();
    expect(winners).toStrictEqual(['jiny', 'mouse']);
  });
});

모든 기능 들에 대해 하나의 테스트 케이스를 가지고 테스트를 진행하고 있었다.

리뷰어 님들중 한 분께서 each() 도입을 적극 추천해주셔서 이번 기회에 학습 후 적용해보게 되었다.

test.each & describe.each(table)(name, fn, timeout)

다른 데이터로 동일한 테스트를 계속 복제하는 경우 test.concurrent.each를 사용하면 테스트를 한 번 작성하고 데이터를 전달할 수 있으며 테스트는 모두 비동기적으로 실행됩니다. - jest docs -

each 메서드의 경우 test와 describe, it에서 적용이 가능하며, 테스트 케이스 또는 각 기능 마다 생성한 테스트 케이스를 통해 검증이 가능하다.

test.each(['jiny,re,ac, t', ' ', 're, a,ct', ' v,u,e', '1, 2, 3'])(
  '%s중 공백이 있는 자동차 이름이 존재하여 Syntax Error 및 에러 메시지가 발생한다.',
  (invalidCase) => {
    expect(() => validateCarNames(invalidCase)).toThrow(ERROR_MESSAGE.INCLUDE_EMPTY_WORDS);
    expect(() => validateCarNames(invalidCase)).toThrow(SyntaxError);
  },
);

describe.each([
  [4, 'jiny,react,vue'],
  [2, 'jine,mouse'],
  [3, 'book,pen,cil'],
  [5, 'apple,panda,fee,conf,cook'],
])('자동차 게임 우승자 출력 테스트', (count, carNames) => {
  test('자동차 게임이 완료되었을 때 우승자는 최소 1명 이상 나올 수 있다.', () => {
    const racingGame = new RacingGame(carNames, count);
    racingGame.race();
    const winners = racingGame.confirmRacingWinners();
    expect(winners.length >= 1).toBeTruthy();
  });
});

이렇게 배열 내부에 검증하고 싶은 테스트 케이스 들을 table 인자 내부에 추가할 수 있다.

실행 과정
1. 콜백 내부의 invalidCase는 배열 내 요소들을 비동기적으로 하나씩 테스트 한다. ('jiny, re,ac, t', ' ' ... )
2. validateCarNames는 저장된 invalidCase를 인자 안에 추가 후 에러가 발생하는 지 확인한다.
3. toThrow matcher를 통해 의도 된 에러가 나왔는지 검증 한다.

실제로 테스트 해보면 모든 테스트 케이스 들이 테스트가 진행 된 것을 확인할 수 있다.

  • %p - pretty-format.
  • %s - String.
  • %d - Number.
  • %i - Integer.
  • %f - Floating point value.
  • %j - JSON.
  • %o - Object.
  • %# - Index of the test case.
  • %% - single percent sign ('%'). This does not consume an argument.

또한, name에 변환 문자를 통해 table에 입력한 값들을 테스트 케이스에 추가하여 마치 JS의 template literal 처럼 사용이 가능하다.

re-export

리뷰어 님의 step1 코멘트 중 무분별한 re-export를 지양하는 것도 고려해보면 좋을거 같다는 의견을 주셨다.

step2 모든 곳에서 re-export를 사용하고 있었기 때문에 다시 한번 생각해보게 되었다.

장점

  • 가독성이 좋아진다.

단점

  • re-export가 많아지면 트리쉐이킹에 좋지 않다.
  • 너무 무분별한 re-export는 가독성에 좋지 않다.
// re-export
import {
 AVALIABLE_RANDOM_NUMBER,
 CAR_MAX_LENGTH,
 CAR_MIN_LENGTH,
 ERROR_MESSAGE,
 INPUT_MESSAGE,
 SEPERATOR_SYMBOLS,
} from '../src/constants';

// no re-export
import { ERROR_MESSAGE, INPUT_MESSAGE } from '../src/constants/message.js';
import { AVALIABLE_RANDOM_NUMBER } from '../src/constants/randomNumber.js';
import { SEPERATOR_SYMBOLS } from '../src/constants/commons.js';
import { CAR_MAX_LENGTH, CAR_MIN_LENGTH } from '../src/constants/validate.js';

constants 폴더의 경우 message(입/출력, 에러 cli에서 확인하기 위한 메시지), randomNumber(랜덤 숫자 생성 관련), commons(공통으로 사용되는 것들), validate(검증을 위한 상수 값)로 나누고 있었다.

re-export를 하게 되면 위 변수 들이 어떤 역할을 하는 변수 들인지 아래 보다 파악이 어려운 것을 확인할 수 있다.

그래서, constants, utils 폴더의 경우 각 파일 네이밍이 역할과 관련된 모듈이기 때문에 re-export를 제거하게 되었다.

static field & method

static 키워드에 대한 아티클 디깅이 필요한 것 같다고 코멘트 해주셔서 이번 기회에 static 키워드가 갖는 의미에 대해 학습했다.

인스턴스 내 helper 함수로 사용할 경우

static #createNewResult(newMoveStatus) {
  return Object.entries({ ...newMoveStatus })
    .map((racerInfo) => racerInfo.join(SEPERATOR_SYMBOLS.COLON))
    .join(SEPERATOR_SYMBOLS.NEW_LINE);
}

RacingGame 클래스의 createNewResult 함수의 경우 Car의 moveStatus 의 자료 구조를 게임 결과로 원하는 자료 구조에 알맞게 반환하는 역할을 수행 한다.

이 처럼 인스턴스 내 자료 구조를 변경하거나 값의 데이터 타입을 바꾸는 등의 util 작업을 할 때 static 키워드가 유용하게 사용될 수 있다.

GameController.js

static async #getRacingCount() {
  try {
    const racingCount = await InputView.input(INPUT_MESSAGE.COUNT);
    Validator.check(racingCount, INPUT_MESSAGE.COUNT);
    return racingCount;
  } catch (error) {
    OutputView.print(error.message);
    return GameController.#getRacingCount();
  }
}

getRacingCount의 경우 view에서 받은 데이터를 검증 후 에러 처리 후 다시 입력 받게 하거나 데이터를 반환하는 controller에서 중요한 역할을 하는 함수 임에도 불구하고 field를 참조하지 않는다는 이유로 static 키워드를 사용하여 표현하고 있었다.

export class GameController {
  #racingGame;

  #view;

  constructor() {
    this.#view = new View();
  }

  async #inputRacingCount() {
    return this.#retryOnErrors(async () => {
      const racingCount = await this.#view.inputByUser(INPUT_MESSAGE.COUNT);
      Validator.check(racingCount, INPUT_MESSAGE.COUNT);
      return racingCount;
    });
  }
}

컨트롤러 인스턴스에 View를 추가 후 view를 참조함을 통해 static 키워드를 제거함으로써 문제를 해결할 수 있었다.

inputview, outputview를 하나의 view 클래스에서 관리

View.js

import { InputView, OutputView } from './index.js';

export class View {
  #inputView;

  #outputView;

  constructor() {
    this.#inputView = InputView;
    this.#outputView = OutputView;
  }

  async inputByUser(message) {
    const userInput = await this.#inputView.input(message);
    return userInput;
  }

  print(message) {
    this.#outputView.print(message);
  }
}

InputView와 OutputView를 하나의 View 클래스에서 관리할 수 있도록 하였다.

GameController.js

// case 1
export class GameController {
  constructor() {
    this.#inputView = InputView;
    this.#outputView = OutputView;
    this.#specificView = SpecificView;
  }
}


// case 2 
export class View {
  constructor() {
    this.#inputView = InputView;
    this.#outputView = OutputView;
    this.#specificView = SpecificView;
  }
}

export class GameController {
  constructor() {
    this.view = new View();
  }
}

장점

  • Controller에서 수정하는 것이 아닌 View에서 관리 할 수 있기 때문에 관심사의 분리가 더 명확해진다.
  • 현재 애플리케이션 에서는 모든 도메인에 대해 View를 하나로 대응하기 때문에 가독성 측면에서 문제 될 것이 없다.
  • Controller는 OutputView인지, InputView인지 모르기 때문에 도메인에 얽히지 않고 필요한 메서드를 view에서 제공 받을 수 있다.

View를 도입할 때 얻을 수 있는 이점은 다음과 같았다.

무엇보다 좋았던 점은 Controller가 더 이상 View에게 "어떻게"가 아닌 "무엇"을 원해서 메서드를 사용할 것인지 구분 지을 수 있어서 좋았다.

0개의 댓글