1주차 프로젝트(문자열 덧셈 계산기) 테스트 코드 분석

JS K·2025년 10월 17일
post-thumbnail

테스트 코드를 분석하게 된 이유

테스트 코드를 분석해야
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]');
  });
});

이 테스트는 다음 흐름으로 작동한다.

  1. mockQuestions()가 입력을 가짜로 생성해 readLineAsync()를 대체한다.
  2. getLogSpy()print() 호출을 감시한다.
  3. App.run()을 실행하면 실제로 입력을 받는 대신 우리가 만든 mock이 응답한다.
  4. 정상 입력일 때는 출력 문자열을, 비정상 입력일 때는 [ERROR] 예외를 검증한다.





mockQuestions

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

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()은 어떤 구조일까?

이 테스트가 통과하려면 App.run() 내부는 다음과 같은 흐름이면 충분하다.

// 1. MissionUtils.Console.readLineAsync()로 사용자 입력 받기
// 2. 커스텀 구분자가 있는 지 확인
// 3. 커스텀 모드냐 아니냐에 따라 split 하기
// 4. 입력값에 오류가 있으면(음수가 포함 등) 있으면 에러 던지기
// 5. 합계를 MissionUtils.Console.print()로 출력
  • 정상 입력: "//;\n1"결과 : 1 출력
  • 비정상 입력: "-1,2,3"[ERROR] 예외 발생





jest 간단한 예시

상황예시 코드설명
값 비교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이 뭔데?!

mock(모킹)
테스트에서 진짜 함수 대신 가짜 함수를 만들어,
외부 동작(입력, 출력, 네트워크 등)을 흉내내는 것
이다.




🎯 예시 1: 함수 호출 감시하기

/*
  상황: 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을 쓰면 “실행된 척”만 하고 호출 여부를 확인할 수 있다.




🚀 예시 2: 외부 API를 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()을 어떤 흐름으로 구현해야 할지”
감이 잡혔다.
즉, 테스트 코드가 프로그램이 가져야 할 기능과 구조를 미리 정의하고 있었던 것이다.

이 경험 덕분에

테스트 코드는 단순히 검증 도구가 아니라,
프로젝트의 설계를 이끌어가는 지침서

라는 걸 느꼈다.

앞으로는 나도 좋은 테스트 코드를 작성할 수 있는 개발자가 되고 싶다.
그래서 📘 《단위 테스트의 기술》 이라는 책을 구매해,
테스트 코드 작성법을 좀 더 깊이 있게 공부해볼 생각이다.




정리하며

테스트 코드는 단순히 검증 도구가 아니라,
프로젝트의 설계를 이끌어가는 지침서
profile
1.01^365

0개의 댓글