--- id: testing-mocking-boundaries title: Mock 경계 — 어디서 모킹할까 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [testing, mocking, boundaries, vibe-coding] tech_stack: { language: "TypeScript / Jest / Vitest", applicable_to: ["Backend", "Web"] } applied_in: [] aliases: [test double, fake, stub, spy, msw] --- # Mock 경계 > 모킹은 **외부 시스템 경계** 에서. 자기 코드 모킹 = test 가 코드 모양을 검증할 뿐 행위 안 검증. 외부 의존성 (HTTP, DB, time, fs) 은 모킹 OK 하되 **integration test 에서는 진짜 사용** 보완. ## 📖 핵심 개념 Test double 5종: - **Dummy**: 자리만 채움. - **Stub**: 정해진 값 반환. - **Spy**: 호출 기록 + 정해진 값. - **Mock**: 호출 기대 검증. - **Fake**: 진짜 동작 흉내 (in-memory DB). ## 💻 코드 패턴 ### MSW — HTTP 모킹 (web/node 통합) ```ts import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; const server = setupServer( http.get('https://api.example.com/users/:id', ({ params }) => { return HttpResponse.json({ id: params.id, email: `user${params.id}@a.com` }); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); test('fetch user', async () => { const u = await fetchUser('1'); expect(u.email).toBe('user1@a.com'); }); ``` ### Time 모킹 ```ts import { vi } from 'vitest'; beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); test('debounce 300ms', () => { const fn = vi.fn(); const debounced = debounce(fn, 300); debounced(); debounced(); debounced(); vi.advanceTimersByTime(300); expect(fn).toHaveBeenCalledTimes(1); }); ``` ### 의존성 주입 — fake repository ```ts interface UserRepo { find(id: string): Promise; } class InMemoryUserRepo implements UserRepo { private store = new Map(); async find(id: string) { return this.store.get(id) ?? null; } seed(u: User) { this.store.set(u.id, u); } } test('service uses repo', async () => { const repo = new InMemoryUserRepo(); repo.seed({ id: '1', email: 'a@a.com' }); const svc = new UserService(repo); expect(await svc.greet('1')).toBe('Hi a@a.com'); }); ``` ### Spy on existing module ```ts import * as mailer from './mailer'; test('sends welcome email', async () => { const sendSpy = vi.spyOn(mailer, 'sendEmail').mockResolvedValue(); await registerUser({ email: 'a@a.com' }); expect(sendSpy).toHaveBeenCalledWith({ to: 'a@a.com', template: 'welcome' }); }); ``` ## 🤔 의사결정 기준 | 의존성 | 권장 | |---|---| | HTTP (외부 API) | MSW (request-level) | | DB | testcontainers (real Postgres) — unit 에서는 in-memory fake | | Time | fake timers | | Random | seeded faker | | Filesystem | memfs 또는 tmpdir | | 자기 비즈니스 로직 | ❌ 모킹 X. 진짜 호출 | | Network 인접 (queue producer) | spy on enqueue | | 큰 외부 SDK (AWS, Stripe) | 외부 wrapper + wrapper interface 모킹 | ## ❌ 안티패턴 - **자기 함수 모킹** (`jest.mock('./foo')`): 통합 안 됨. test 가 implementation 모양만 검증. - **모든 곳 mock**: 가짜 통과. integration test 안 함. - **mock 상태 누수**: `beforeEach reset` 누락. 다음 test 영향. - **MSW + jest.fn() 양쪽 사용**: 일관성 깨짐. HTTP 는 MSW 한 군데로. - **모킹된 인터페이스가 실제와 다른 형태**: deploy 후 production 에서 폭사. integration 검증. - **시간을 `Date.now()` 직접 모킹**: 부작용 큼. fake timers. - **"that's mocked" 라고 말하면서 진짜 호출**: jest.spyOn vs jest.mock 차이 명확. ## 🤖 LLM 활용 힌트 - 외부 API = MSW. DB = testcontainers. 자기 코드 = 모킹 X. - 의존성 주입 + in-memory fake 가 가장 깔끔한 unit test. ## 🔗 관련 문서 - [[Testing_Test_Pyramid]] - [[Testing_Contract_Testing]]