본문으로 건너뛰기
yutils

mocking 은 어떻게 동작할까?

stub vs mock vs spy vs fake (Gerard Meszaros 의 4종), over-mocking 이 진짜 아무것도 100% 커버 안 하는 이유, network vs DB 가짜 시점, test double 이 내부 구현과 결합되는 함정.

약 9분 읽기

test 에서 외부 의존 — DB, HTTP, file IO, 시간 — 을 fake 로 대체. "그냥 mock 하면 되지" 는 단순해 보이지만, test double 의 4 종 (stub / mock / spy / fake) 와 각각의 cost 가 다름. 이 가이드는 Gerard Meszaros 의 분류, mock 의 함정, "100% coverage 인데 진짜 안전 안 함" 이 일어나는 이유를 정리한다.

Test Double — 4 종 (Gerard Meszaros)

1. Stub — 미리 정한 값 반환

class UserServiceStub {
  findUser(id) {
    return {id: 1, name: "Alice"};  // 항상 같은 값
  }
}

test("displays user name", () => {
  const view = renderUserView(new UserServiceStub());
  assert(view.contains("Alice"));
});

용도: "이 시점에 이 값이 와야 한다" 가정하고 다음 로직 검증.
호출 검증 X — return value 만 중요.

2. Mock — 호출도 검증

const mockEmail = mock();
processOrder(order, mockEmail);
expect(mockEmail).toHaveBeenCalledWith(
  "alice@example.com",
  expect.stringContaining("confirmation")
);

용도: "이 의존성이 정확한 인자로 호출됐는가" 검증.
side effect (이메일 발송, 결제 등) 검증에 적합.

3. Spy — 진짜 객체 + 호출 추적

const realLogger = new ConsoleLogger();
const spy = jest.spyOn(realLogger, "warn");

doWork(realLogger);

expect(spy).toHaveBeenCalledTimes(2);
// realLogger 는 실제 console 로 출력도 함 (default)

용도: 진짜 동작 유지하면서 호출 사실만 별도 검증.
또는 일부 method 만 mock (spy + mockReturnValue).

4. Fake — 동작하는 단순 구현

class InMemoryUserRepo {
  users = new Map();
  save(u) { this.users.set(u.id, u); }
  find(id) { return this.users.get(id); }
}

// 진짜 DB 와 같은 API, 메모리 안에서만 동작
test("user saved and findable", () => {
  const repo = new InMemoryUserRepo();
  repo.save({id: 1, name: "Alice"});
  expect(repo.find(1).name).toBe("Alice");
});

용도: 무겁고 느린 dependency (DB, queue) 의 가벼운 대체.
testcontainer / sqlite in-memory 와 같은 영역.

Over-Mocking — "100% coverage 인데 진짜 안전 X"

function createOrder(user, items) {
  const total = items.reduce((s, i) => s + i.price * i.qty, 0);
  return {user, items, total};
}

test("creates order", () => {
  const user = mock();
  const items = [mock()];

  // items[0] 의 price, qty 가 mock 이라 동작 안 됨
  // 또는 mock(items[0].price 도 다시 mock)

  const order = createOrder(user, items);
  expect(order.user).toBe(user);  // ← tautology
  expect(order.items).toBe(items);  // ← tautology

  // total 검증은? 다 mock 이라 의미 X
});

문제:
- 100% 코드 line 실행
- 그러나 진짜 계산은 검증 X (input/output 모두 mock)
- coverage tool 은 그린, 진짜 버그 잡힘 X

Mocking 의 정답 경계

✓ Mock 가치 큰 영역

  • 외부 network (HTTP API, third-party service)
  • 시간 (Date.now, setTimeout)
  • random (Math.random)
  • filesystem (cross-platform 차이)
  • 비싼 side effect (이메일, SMS, 결제)

✗ Mock 피해야 할 영역

  • plain function / pure logic (직접 호출이 더 쉬움)
  • data structure (mock 보다 fixture 객체)
  • own code 의 내부 module (구현 결합 ↑)
  • DB — fake (in-memory) 또는 testcontainer 가 더 안전

Time / Random Mock — 특수 영역

// 시간 mock
jest.useFakeTimers();
jest.setSystemTime(new Date("2026-05-25T00:00:00Z"));

test("expires after 1 hour", () => {
  const session = createSession();
  jest.advanceTimersByTime(60 * 60 * 1000 + 1);
  expect(session.expired()).toBe(true);
});

// random mock — 비결정적 test 회피
jest.spyOn(Math, "random").mockReturnValue(0.5);

→ test 결정성 확보. random 가 들어간 logic 도 reproducible 검증.

Mock 의 구현 결합 함정

// 코드
async function getUser(id) {
  const cached = await cache.get(id);
  if (cached) return cached;
  const user = await db.users.find(id);
  await cache.set(id, user);
  return user;
}

// test (over-specified)
test("fetches user", async () => {
  cache.get.mockResolvedValue(null);
  db.users.find.mockResolvedValue({id: 1, name: "A"});
  cache.set.mockResolvedValue();

  const user = await getUser(1);

  expect(cache.get).toHaveBeenCalledWith(1);
  expect(cache.get).toHaveBeenCalledTimes(1);  // ← 구현 결합
  expect(db.users.find).toHaveBeenCalledWith(1);
  expect(cache.set).toHaveBeenCalledWith(1, {id: 1, name: "A"});
  expect(user.name).toBe("A");
});

문제:
- caching 로직 refactor (예: cache.getOrSet 단일 호출) → test 깨짐
- 진짜 검증 = "user 반환" 뿐인데 5 assertion
- 구현 변경마다 test 수정 부담

좋은 test:
test("returns the user", async () => {
  db.users.find.mockResolvedValue({id: 1, name: "A"});
  const user = await getUser(1);
  expect(user.name).toBe("A");
});

Test Double 선택 가이드

상황도구
return value 만 필요Stub
호출 자체가 검증 대상 (이메일·결제)Mock
진짜 동작 + 호출 추적 둘 다Spy
가벼운 in-memory 대체 필요Fake
외부 HTTP APInock / msw (HTTP-level mock)
DBtestcontainer / sqlite in-memory (fake)
시간jest.useFakeTimers / sinon

흔한 함정

  • plain function 도 mock — 직접 import + 호출이 더 쉽고 안전.
  • 구현 detail 검증 — "cache.get 이 1번 호출됐다" 같은 over-specified. behavior 만 검증.
  • mock 의 reset 누락 — test 간 state 누수 → flaky. beforeEach 의 jest.clearAllMocks().
  • auto-mock 남용 — 모든 module 을 자동 mock 하면 어디서 진짜 통합 안 되는지 안 보임. 명시적 mock 권장.
  • real network 대신 stub 만 — HTTP API 변경 시 production 만 깨짐. contract test (별도 가이드) 보강.

마무리

Mocking 의 본질 = "통제 어려운 의존을 통제 가능한 fake 로". 다만 과하면 구현 결합 + 진짜 통합 검증 누락. test double 의 4 종을 의도적으로 선택.

실용 — 외부 boundary (HTTP, time, random) 만 mock, 내부 logic 은 직접 호출. DB 는 fake (in-memory) 또는 testcontainer. behavior 검증 (output 기반), 구현 검증 (호출 횟수 등) 회피.

가이드 목록으로