
테스트 코드를 분석해야
1주차 프로젝트의 윤곽이 보일 것 같았다.
특히... App.run()이 대체 뭐하는 건지
입력은 어떻게, 출력은 어떻게 되어야하는건지
전혀 감도 안잡혔기에
개발방향을 조금이라도 잡고 싶어서
테스트 코드 분석을 하게 시작하게 되었다.
테스트 파일은 다음과 같은 구조로 이루어져 있다.
테스트의 목적은 App.run() 메서드가 주어진 입력에 대해 예상한 출력 혹은 에러를 내는지 확인하는 것이다.
import App from '../src/App.js';
import { MissionUtils } from '@woowacourse/mission-utils';
// 🧩 사용자 입력을 모킹하는 함수
const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn();
MissionUtils.Console.readLineAsync.mockImplementation(() => {
const input = inputs.shift();
return Promise.resolve(input);
});
};
// 🧩 출력(print)을 감시하는 함수
const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, 'print');
logSpy.mockClear();
return logSpy;
};
// 🧪 테스트 전체 그룹: 문자열 계산기
describe('문자열 계산기', () => {
// ✅ 정상 입력 케이스: 커스텀 구분자 사용
test('커스텀 구분자 사용', async () => {
const inputs = ['//;\n1'];
mockQuestions(inputs);
const logSpy = getLogSpy();
const outputs = ['결과 : 1'];
const app = new App();
await app.run();
outputs.forEach((output) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});
// ⚠️ 비정상 입력 케이스: 음수 입력 시 에러 발생
test('예외 테스트', async () => {
const inputs = ['-1,2,3'];
mockQuestions(inputs);
const app = new App();
await expect(app.run()).rejects.toThrow('[ERROR]');
});
});
이 테스트는 다음 흐름으로 작동한다.
mockQuestions()가 입력을 가짜로 생성해 readLineAsync()를 대체한다. getLogSpy()로 print() 호출을 감시한다. App.run()을 실행하면 실제로 입력을 받는 대신 우리가 만든 mock이 응답한다. [ERROR] 예외를 검증한다.mockQuestions는 입력받는 함수를 가짜로 바꾸는(mocking) 함수다.
사용자가 콘솔에서 직접 타이핑하지 않아도, 테스트가 자동으로 입력을 흉내내도록 만들어준다.
const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn().mockImplementation(() => {
const input = inputs.shift();
return Promise.resolve(input);
});
};
이 코드는 이렇게 작동한다 👇
| 단계 | 동작 | 설명 |
|---|---|---|
| ① | mockQuestions(['1,2,3']) 실행 | 입력 배열 설정 |
| ② | 코드 내부에서 MissionUtils.Console.readLineAsync() 호출됨 | 실제 입력 대신 mock 함수가 실행됨 |
| ③ | inputs.shift()로 '1,2,3'을 꺼내 반환 | 사용자가 입력한 것처럼 Promise로 전달 |
| ④ | 프로그램은 '1,2,3'을 입력받은 것처럼 동작 | 테스트에서 자동화된 입력 시뮬레이션 완료 |
즉, readLineAsync()가 실제로는 아무 입력도 받지 않지만,
테스트에서는 우리가 준비한 값을 그대로 반환하는 가짜 입력기로 바뀐다.
getLogSpy()는 출력 함수(print)가 제대로 호출되었는지 감시(spy) 하는 역할이다.
const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, 'print');
logSpy.mockClear();
return logSpy;
};
이 스파이는 print가 호출될 때의 인자값, 호출 횟수, 순서까지 추적할 수 있다.
예시 👇
const logSpy = getLogSpy();
MissionUtils.Console.print('결과 : 3');
expect(logSpy).toHaveBeenCalled(); // ✅ print가 호출되었는가
expect(logSpy).toHaveBeenCalledWith('결과 : 3'); // ✅ 특정 메시지로 호출되었는가
테스트 코드에서는 이 스파이를 이용해,
App.run()이 "결과 : 1" 같은 메시지를 출력했는지 확인한다.
test('커스텀 구분자 사용', async () => {
const inputs = ['//;\n1'];
mockQuestions(inputs);
const logSpy = getLogSpy();
const app = new App();
await app.run();
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('결과 : 1'));
});
'//;\n1' (커스텀 구분자 ; 사용) mockQuestions(inputs) → 입력 흉내 logSpy → 출력 감시 "결과 : 1" 문자열 출력 여부 확인 ✅ 정상 입력 시 결과가 제대로 출력되는지 검증
test('예외 테스트', async () => {
const inputs = ['-1,2,3'];
mockQuestions(inputs);
const app = new App();
await expect(app.run()).rejects.toThrow('[ERROR]');
});
'-1,2,3' (음수 포함) app.run() 실행 시 Promise가 reject되어야 함 [ERROR] 포함되어야 함 ✅ 비정상 입력 시 에러를 올바르게 던지는지 검증
이 테스트가 통과하려면 App.run() 내부는 다음과 같은 흐름이면 충분하다.
// 1. MissionUtils.Console.readLineAsync()로 사용자 입력 받기
// 2. 커스텀 구분자가 있는 지 확인
// 3. 커스텀 모드냐 아니냐에 따라 split 하기
// 4. 입력값에 오류가 있으면(음수가 포함 등) 있으면 에러 던지기
// 5. 합계를 MissionUtils.Console.print()로 출력
"//;\n1" → 결과 : 1 출력 "-1,2,3" → [ERROR] 예외 발생| 상황 | 예시 코드 | 설명 |
|---|---|---|
| 값 비교 | expect(2 + 2).toBe(4) | 값이 정확히 같은지 |
| 함수 에러 검사 | expect(() => f()).toThrow('[ERROR]') | 함수가 에러를 던지는지 |
| 비동기 검사 (reject 여부) | await expect(asyncFn()).rejects.toBeDefined() | Promise가 reject되는지 |
| mock 호출 검사 | expect(mockFn).toHaveBeenCalledWith('결과') | mock 함수가 특정 인자로 호출되었는지 |
expect()는 검사 대상(값, 함수, Promise 등)을 감싸며,
뒤에 .toBe(), .toThrow(), .rejects 등의 매처(matcher) 를 붙여서 기대 조건을 표현한다.
mock(모킹) 은
테스트에서 진짜 함수 대신 가짜 함수를 만들어,
외부 동작(입력, 출력, 네트워크 등)을 흉내내는 것이다.
/*
상황: greet 함수를 테스트 하고 싶다.
조건 : makeMessage 함수는 아직 구현되지 않았다.
greet 함수를 호출할 때
테스트1: makeMessage 함수가 호출되었는지,
테스트2: 호출되었다면 어떤 인수와 함께 호출되는지 확인
*/
function greet(name) {
const message = makeMessage(name);
console.log(message);
}
function makeMessage(name) {
return "";
}
test('greet가 makeMessage를 호출한다', () => {
const makeMessage = jest.fn().mockReturnValue('Hello, Test!');
function greet(name) {
const message = makeMessage(name);
console.log(message);
}
greet('Wook');
expect(makeMessage).toHaveBeenCalled();
expect(makeMessage).toHaveBeenCalledWith('Wook');
});
✅ mock이 없으면 진짜
makeMessage()가 실행되지만,
mock을 쓰면 “실행된 척”만 하고 호출 여부를 확인할 수 있다.
/*
상황 : fetchUser 함수를 테스트하고 싶다.
하지만 fetch로 실제 API를 요청하면
어떤 값이 들어올지 예측을 할 수가 없으며
오류가 발생할 수 도 있다.
fetch 함수는 잘 동작 한다고 가정하고
모킹을 해서 특정값을 반환하도록 함 {name:'work"}
그래서 fetchUser 함수를 호출했을 때
테스트1: fetch 함수가 특정 인수로 호출이 되었는지
테스트2: fetch 함수가 data를 제대로 반환했는지
확인하는 테스트 코드를 작성
*/
async function fetchUser() {
const res = await fetch('https://api.example.com/user');
const data = await res.json();
return data;
}
test('fetchUser가 데이터를 받아온다', async () => {
global.fetch = jest.fn().mockResolvedValue({
json: () => Promise.resolve({ name: 'Wook' }),
});
const data = await fetchUser();
expect(fetch).toHaveBeenCalledWith('https://api.example.com/user');
expect(data).toEqual({ name: 'Wook' });
});
이 코드는 실제로 네트워크에 연결하지 않고도
“API를 호출한 것처럼” 테스트를 진행할 수 있다.
| 상황 | mock이 없다면 | mock을 쓰면 |
|---|---|---|
| 함수 호출 | 진짜 실행됨 | 가짜 함수로 대체 |
| 콘솔 출력 | 실제 출력 발생 | 호출만 감시 가능 |
| API 요청 | 진짜 네트워크 요청 발생 | 네트워크 없이 가짜 응답 반환 |
| 테스트 속도 | 느림 | 빠름 |
| 테스트 안정성 | 환경에 영향 받음 | 완전 독립적 |
요약하자면 mock은
“테스트 중 실제로 무언가를 실행하지 않아도,
실행된 것처럼 시뮬레이션할 수 있게 만드는 가짜 객체”다.
기존에는 코드를 먼저 작성하고,
그 코드가 잘 동작하는지 확인하기 위해
직접 여러 값들을 입력하며 동작을 확인하였다.
그런데 이번 1주차 프로젝트에서는
테스트 코드가 먼저 주어지고,
그 테스트를 통과하기 위해 코드를 작성해야 했다.
처음엔
아직 코드도 없는데,
어떻게 테스트부터 만들 수 있지?
라는 의문이 들었다.
하지만 테스트 코드를 분석하면서 깨달았다.
테스트 코드가 이미 프로젝트 설계의 완성본이라는 것.
테스트를 뜯어보니 자연스럽게
“App.run()을 어떤 흐름으로 구현해야 할지”
감이 잡혔다.
즉, 테스트 코드가 프로그램이 가져야 할 기능과 구조를 미리 정의하고 있었던 것이다.
이 경험 덕분에
테스트 코드는 단순히 검증 도구가 아니라,
프로젝트의 설계를 이끌어가는 지침서
라는 걸 느꼈다.
앞으로는 나도 좋은 테스트 코드를 작성할 수 있는 개발자가 되고 싶다.
그래서 📘 《단위 테스트의 기술》 이라는 책을 구매해,
테스트 코드 작성법을 좀 더 깊이 있게 공부해볼 생각이다.
테스트 코드는 단순히 검증 도구가 아니라,
프로젝트의 설계를 이끌어가는 지침서