2주차 미션을 PR 올린 시점에서 2주차에서 시도해보고 새로운 레퍼런스를 얻었던 내용 들에 대해 회고해보려고 한다.
// 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.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 처럼 사용이 가능하다.
리뷰어 님의 step1 코멘트 중 무분별한 re-export를 지양하는 것도 고려해보면 좋을거 같다는 의견을 주셨다.
step2 모든 곳에서 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 키워드에 대한 아티클 디깅이 필요한 것 같다고 코멘트 해주셔서 이번 기회에 static 키워드가 갖는 의미에 대해 학습했다.
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 키워드를 제거함으로써 문제를 해결할 수 있었다.
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에게 "어떻게"가 아닌 "무엇"을 원해서 메서드를 사용할 것인지 구분 지을 수 있어서 좋았다.