우아한테크코스 2주차에 접어들었다! 이번주 미션은 자동차 경주 게임이다. 자세한 미션은 이곳에서 확인할 수 있다.
feat: 한글 메시지
형식을 사용했는데 feat(App): english message
형식을 사용해야 한다이번 미션은 저번 미션에서 학습한 클래스와 객체 지향을 활용하며, Jest
를 파보기로 결심했다. 그리고 어차피 테스트 하는 거 이왕이면 TDD
를 시도해볼 생각이다. 일주일 후의 내가 테스트에 익숙한 사람이 되길 바라며...
feat(App): english message
미션의 리드미를 읽고 일단 어떤 클래스들을 활용할 것인지 설계해보았다. 다른 것보다도 결과물은 어떻게 달라질지 궁금하다.
먼저 작성되어 있는 테스트 코드가 잘 이해가 안 되길래 공식 문서를 읽었다.
toBe
: 실제 값이 기대한 값과 정확히 일치하는지 확인toEqual
: 객체나 배열 같은 데이터 구조의 실제 값과 기대한 값이 동일한 내용을 가지는지 확인toBeNull
: null을 가지는지 확인toBeUndefined
: undefined을 가지는지 확인toBeDefined
: undefined을 가지지 않는지 확인toBeTruthy
: true인지 확인toBeFalsy
: false인지 확인toMatch
: 문자열이 정규표현식과 일치하는지 확인toContain
: 배열 또는 문자열이 특정 값 또는 원소를 포함하는지 확인toThrow
: 함수 호출 시 예외가 발생하는지 확인const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn();
MissionUtils.Console.readLineAsync.mockImplementation(() => {
const input = inputs.shift();
return Promise.resolve(input);
});
};
MissionUtils.Console.readLineAsync
대신 inputs
배열 각 호출에 대하여 input
배열로 return한다.
const inputs = ["pobi,woni", "1"];
mockQuestions(inputs);
const mockRandoms = (numbers) => {
MissionUtils.Random.pickNumberInRange = jest.fn();
numbers.reduce((acc, number) => {
return acc.mockReturnValueOnce(number);
}, MissionUtils.Random.pickNumberInRange);
};
MissionUtils.Random.pickNumberInRange
대신 numbers 배열에 있는 각 숫자를 순차적으로 반환한다.
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
mockRandoms([4, 3]);
Random.pickNumberInRange
: 4 반환Random.pickNumberInRange
: 3 반환const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, "print");
logSpy.mockClear();
return logSpy;
};
MissionUtils.Console.print
의 함수 호출 여부, 호출 횟수, 전달된 인수를 확인할 수 있다.
const video = {
play() {
return true;
},
};
test('plays video', () => {
const spy = jest.spyOn(video, 'play');
const isPlaying = video.play();
expect(spy).toHaveBeenCalled();
expect(isPlaying).toBe(true);
});
const outputs = ["pobi : -"];
const logSpy = getLogSpy();
outputs.forEach((output) => {
// MissionUtils.Console.print가 output을 포함한 string 인수와 호출되었는지 확인
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
describe("Math operations", () => {
test.each([
[1, 2, 3], // Input: 1, 2, Expected Output: 3
[5, 5, 10], // Input: 5, 5, Expected Output: 10
[0, 0, 0], // Input: 0, 0, Expected Output: 0
])("add(%i, %i) returns %i", (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
});
test.each([
[["pobi,javaji"]],
[["pobi,eastjun"]]
])("이름에 대한 예외 처리", async (inputs) => {
// given
mockQuestions(inputs);
// when
const app = new App();
// then
await expect(app.play()).rejects.toThrow("[ERROR]");
});
이제 어느 정도 jest에 대해 알게 된 것 같으니 본격적으로 jest를 활용해보자.
일단 가장 단순한 메서드인 자동차 이름 입력(성공)에 대한 테스트 코드를 작성했다.
class InputHandler {
static async getCarNameArray() {
const inputStr = await Console.readLineAsync(MESSAGE.ENTER_CAR_NAMES);
return inputStr.split(",");
}
}
test.each([
["pobi,woni,joo,java", ["pobi", "woni", "joo", "java"]],
["pobi,woni", ["pobi", "woni"]],
["java,woo,wa,han,tech", ["java", "woo", "wa", "han", "tech"]],
["joo", ["joo"]],
])("유효한 자동차 이름 입력 - %s", async (input, expectedOutput) => {
mockQuestions([input]);
const inputHandler = new InputHandler();
const result = await inputHandler.getCarNameArray();
expect(result).toEqual(expectedOutput);
expect(MissionUtils.Console.readLineAsync).toHaveBeenCalledWith(
MESSAGE.ENTER_CAR_NAMES
);
});
test.each()
를 활용했는데 테스트 제목 형식에 printf 서식을 사용해 매개변수를 삽입해야 하는 것을 처음에는 알지 못해서 밤새 헤맸다. 😨 심지어 테스트는 다 통과되고 있었어서 나중에 잘못된 테스트 케이스가 통과되는 것을 보고 잘못됐다는 것을 알았다. 아무튼 이제라도 알아서 다행!
printf 서식은 다음과 같다.
%p
: pretty-format%s
: String%d
: Number%i
: Integer%f
: Floating point value%j
: JSON%o
: Object%#
: Index of the test case결과는 성공! 쉬워보이지만 jest가 처음이라 너무너무 뿌듯하다. ㅋㅋㅋㅋ
이후 나머지 기능 구현을 해주었다. 결과적으로는 아래와 같은 구조로 기능을 구현했다. 클래스의 관심사를 분리하고 의존성 역전 원칙을 지키려고 노력했다.
그리고 클래스마다 테스트를 작성해주었다!
테스트 초안 작성 -> 기능 구현 -> 테스트 -> 기능 보충 -> 다시 테스트
사실 이렇게 하는 게 TDD인지는 잘 모르겠다 어쨌든 내가 생각한 TDD 방식으로 진행했는데 기능을 구현하면서 일일이 이미 구현한 기능에 영향을 줬는지 수작업으로 테스트할 필요가 없다는 것이 매우!! 유용했다 왜 테스트 하는지 알 것 같다
test.each()
이슈는 나에게 너무 큰 위기였다 사용하는 기능의 공식 문서를 정말 꼼꼼히! 읽을 필요가 있다제 코드는 깃허브에서 확인할 수 있습니다