티스토리 뷰
우아한테크코스 6기 과정 중 프리코스 과정이 시작되었다.
첫 번째 미션으로 "숫자 야구" 미션을 진행하였다. 프리코스 과정은 마치 "Jest"를 알고 있는 것을 전재로 시작하는 기분이었다. 미션 실행을 Jest로 다루게 되어 있었다.
코드를 살펴보고 나서 느낀 것은 코드 내용은 알 수 없지만, 이름을 보고 대략적으로 "어떤 기능을 수행하겠구나" 싶었다.
테스트 코드를 대략적으로 이해는 하고 있어야 겠다는 생각이 들어 검색을 통해 기본적으로 필요한 내용만 찾아보았다.
대충 테스트 코드는 다음과 같았다.
const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn();
MissionUtils.Console.readLineAsync.mockImplementation(() => {
const input = inputs.shift();
return Promise.resolve(input);
});
};
"jest.fn()" 메소드를 통해 가짜 함수(mock function)를 만들 수 있으며, 위의 코드에서는 "MissionUtils.Console.readLineAsync"를 모킹하도록 되어 있다.
아직 어떤 값을 리턴할 지 모킹 함수를 정의하지 않았기 때문에 호출하여도 undefined를 반환한다.
"mockImplementation()" 메소드를 통해 모의 함수 동작을 구현한다. "inputs.shift()"를 사용하여 "inputs"에서 다음 입력 값을 가져온다. 이 배열의 첫 번째 원소가 가져와지고 이는 배열에서 제거된다.
"Promise.resolve(input)"를 사용해 해당 입력 값을 Promise로 래핑하고 반환한다. 이는 비동기적으로 입력 값을 반환하게 된다.
const mockRandoms = (numbers) => {
MissionUtils.Random.pickNumberInRange = jest.fn();
numbers.reduce((acc, number) => {
return acc.mockReturnValueOnce(number);
}, MissionUtils.Random.pickNumberInRange);
};
"jest.fn()" 메소드를 통해 "MissionUtils.Random.pickNumberInRange"를 모킹한다.
"mockReturnValueOnce()" 메소드는 모킹 함수가 호출될 때 특정한 값을 반환하도록 설정할 수 있으며, reduce 메소드를 통해 numbers 배열의 요소를 순차적으로 반환 값으로 설정할 수 있다. 즉, 첫 번째 number는 배열의 첫 번째 요소에 대한 반환 값을 설정하고, 두 번째 number는 두 번째 요소에 대한 반환 값을 설정하는 순으로 반환 값이 순서대로 설정된다.
const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, "print");
logSpy.mockClear();
return logSpy;
};
함수의 호출을 스파이하는 "spyOn(object, methodName)" 메소드는 첫 번째 매개변수인 "object"의 메소드 즉, 두 번째 매개변수인 object의 특정 메소드(methodName)를 스파이화하는 데 사용된다. 이 함수를 호출하면 해당 메소드의 호출을 감시하게 되고, mock function을 반환한다.
object는 스파이화하려는 객체의 인스턴스를 전달하는데, 반드시 특정 메소드를 가지고 있어야 한다. methodName은 object에 존재하는 메소드 중 스파이화하려는 메소드이다.
"jest.spyOn()"으로 생성된 스파이 함수는 원본 메소드를 대체하여, 메소드 호출 확인, 호출 횟수, 호출 매개변수 등을 확인할 수 있다.
그리고 "mockClear()" 메소드는 Mock 함수에 대한 정보를 초기화하는 데 사용된다. Mock 함수의 호출 정보, 인스턴스 정보, 컨텍스트 정보 및 결과 정보를 모두 삭제한다. 주로 두 번 이상 단언 사이에서 Mock 함수의 사용 데이터를 청소하고 싶을 때 사용된다.
위 테스트에서는 "게임 종료 후 재시작", "예외 테스트"를 모두 진행하기 때문에 "mockClear()" 메소드를 선언해 이전 테스트에서 스파이에 의해 기록 정보를 지우고, 현재 테스트에서 스파이를 사용하기 위한 초기 상태로 설정한다. 테스트 간 상태가 격리되어 각 테스트가 독립적으로 실행될 수 있다.
describe("숫자 야구 게임", () => {
test("게임 종료 후 재시작", async () => {
// given
const randoms = [1, 3, 5, 5, 8, 9];
const answers = ["246", "135", "1", "597", "589", "2"];
const logSpy = getLogSpy();
const messages = ["낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료"];
mockRandoms(randoms);
mockQuestions(answers);
// when
const app = new App();
await expect(app.play()).resolves.not.toThrow();
// then
messages.forEach((output) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});
그리고, 동작되어야 할 실제 테스트 코드를 보면, "mockRandoms(randoms)"와 "mockQuestions(answer)"의 반환 값이 무엇인지 대략적으로 알 수 있게 된다.
위 테스트에서 보면, 테스트 코드에 의해 숫자야구를 처음 실행했을 때 맞혀야 할 테스트 케이스는 "[1, 3, 5]"가 된다.
그리고 "answers"를 보면 "Console.readLineAsync()" 메소드가 호출될 때마다 0번 째 인덱스 요소부터 마지막 인덱스 요소까지 입력 값을 Promise로 래핑하고 반환한다.
그리고 처음 보이는 메소드들을 볼 수 있다.
- expect() - 테스트할 대상을 감싸고 검증하는 역할
- 테스트 대상을 받아오게 될 때, 이 경우 "app.play()" 메소드의 실행 결과를 검증한다.
- resolves - 비동기 코드가 Promise를 반환할 때 사용
- "resolves"는 해당 Promise가 해결될 때까지 기다리고, 그 결과를 테스트 한다. 즉, "app.play()" 메소드가 반환하는 Promise가 해결되는지 검증한다.
- toThrow() - 주어진 코드 블록이 실행될 때 예외를 발생시키는지 검증
- 이 경우 "not"과 함께 사용되어, "app.play()" 메소드가 예외를 발생시키지 않아야함을 검증한다.
종합적으로, "app.play()" 메소드가 Promise를 반환하고 이 Promise가 예외를 발생시키지 않고 정상적으로 해결되어야 함을 말한다. 이를 통해 실행 중 예외가 발생하지 않고 제대로 동작하는지 테스트하게 된다.
마지막으로, 게임 실행 후 "logSpy"가 각각 어떤 메시지를 출력하는지 테스트하게 된다.
반복 메소드를 사용해 "messages" 배열의 각 요소에 대해 테스트를 수행하게 된다. "logSpy"가 output 메시지를 포함하는 로그 메시지를 출력했는지 검증하게 되며, "toHaveBeenCalledWith()"와 "stringContaining()" 메소드를 통해 검증하게 된다.
- toHaveBeenCalledWith - 모킹된 함수가 특정한 매개변수들과 함께 호출되었는지 검증하는데 사용
- 예를 들면, "expect(fn).toHaveBeenCalledWith(arg1, arg2)"와 같이 사용했을 때, 함수 "fn"이 "arg1"과 "arg2"와 함께 호출되었는지 검사한다.
- 즉, 위 테스트에서는 "expect.stringContaining(output)"를 인자로하여 호출되었는지 검증한다.
- stringContaining(string) - 문자열 검증에 사용되며, 주어진 문자열이 다른 문자열에 포함되어 있는지 확인
- 즉, 위 테스트에서는 "logSpy" 함수가 "output" 문자열을 포함하는 메시지를 출력했는지 검증한다.
구현 코드 간략하게
const computer = [];
while (computer.length < 3) {
const number = MissionUtils.Random.pickNumberInRange(1, 9);
if (!computer.includes(number)) {
computer.push(number);
}
}
숫자 야구에서 맞혀야 할 랜덤 숫자 3개를 중복없이 생성하도록 하였다.
const guess = await MissionUtils.Console.readLineAsync(messageInputNumber);
if (guess.length !== 3) { // 숫자의 길이가 3자리가 아닐 경우
throw new Error(messageNotThreeDigits);
}
if (isNaN(Number(guess))) { // 숫자가 아닐 경우
throw new Error(messageNotNumber);
}
if (new Set(guess).size !== guess.length) { // 중복 숫자가 있는 경우
throw new Error(messageIsOverlap);
}
if (guess < 1 && guess > 9) { // 1-9 사이의 숫자가 아닐 경우
throw new Error(messageNotOneToNine);
}
그리고 사용자가 숫자를 입력하도록 구현하였다. 이 때, 입력한 값에 유효성 검사를 진행하여 최종적으로 검사를 통과했을 때 값을 반환하도록 하였다.
그리고 게임을 실행하여, 사용자가 입력한 값과 랜덤으로 생성된 값을 비교하여 숫자와 위치가 같으면 스트라이크, 다른 위치에 숫자가 있으면 볼, 없으면 아무것도 없다는 의미의 값을 추가했다.
결과적으로 3개의 숫자가 모두 맞을 때 즉, 스트라이크가 3개가 될 때 게임이 종료되며, 게임을 다시 시작할 것인지 완전 종료할 것인지 선택하도록 하는 것으로 구현을 마쳤다.
회고
사실 테스트 코드를 생각해보지 않았다. 그러나, 미션을 처음으로 확인했을 때 테스트 코드로 진행된다는 것을 알게 되었고 코드를 구현하는 시간보다 테스트 코드를 이해하는 시간이 더 오래 걸렸다. 테스트 코드에 대해 알아보고 나서도 사실 구현하느라 바빴다. 구현에만 집중하다보니 예외 처리에 대해 계속해서 에러가 발생했었는데, 알고보니 "[ERROR]"를 작성해야 한다는 것을 나중에야 알았다.
이 때, 테스트 코드를 다시 살펴보면서 구현한 코드를 조금씩 수정하다보니 결과적으로 통과가 되었다. 그리고 남은 시간동안 지저분하게 작성한 코드들을 나름대로 최대한 깔끔하게 보일 수 있도록 리팩토링하는 데 집중하였다.
남은 미션들도 앞으로 테스트 코드 기반으로 진행될텐데, 이 부분에 대해 더욱 공부해야 겠다는 생각이 절실히 들게 됐다.
'부트캠프 > 우아한테크코스' 카테고리의 다른 글
6기 우아한테크코스 프리코스 3주차 회고 (0) | 2023.11.16 |
---|---|
6기 우아한테크코스 프리코스 2주차 회고 (1) | 2023.11.03 |
우아한테크코스 5기 결과 (0) | 2022.12.27 |
[우아한 테크코스 5기 프리코스] FE 4주차 회고록 (0) | 2022.11.23 |
[우아한 테크코스 5기 프리코스] FE 3주차 회고록 (0) | 2022.11.16 |
- Total
- Today
- Yesterday
- Express
- LottieFiles
- Frontend
- DB Error MongooseServerSelectionError
- 스프링
- 깃허브 Merge
- 신입개발자가 준비해야 할 것들
- if(kakao)dev2022
- Singleton
- 포스텍애플디벨로퍼아카데미
- 원티드 프리온보딩
- 최종추가합격
- 개발 이력서 지원 팁
- 싱글톤
- 원티드 프리온보딩 챌린지
- javascript
- 그룹인터뷰후기
- 고민한 부분
- 자바스크립트
- 개발자이력서꿀팁
- Default Branch
- 포스텍애플아카데미
- 프론트엔드 챌린지
- PostechAppleDeveloperAcademy
- React
- 설명회느낌점
- node
- 조코딩과함께
- 코딩테스트 대비
- #포스텍애플디벨로퍼아카데미
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |