Skip to content

목킹은 종속성을 제어된 구현으로 대체할 수 있게 하여 테스트에 필수적입니다. Bun 은 함수 목, 스파이 및 모듈 목을 포함한 포괄적인 목킹 기능을 제공합니다.

기본 함수 목

mock 함수로 목을 생성하세요.

ts
import { test, expect, mock } from "bun:test";

const random = mock(() => Math.random());

test("random", () => {
  const val = random();
  expect(val).toBeGreaterThan(0);
  expect(random).toHaveBeenCalled();
  expect(random).toHaveBeenCalledTimes(1);
});

Jest 호환

대안으로 Jest 에서와 같이 jest.fn() 함수를 사용할 수 있습니다. 동일하게 작동합니다.

ts
import { test, expect, jest } from "bun:test";

const random = jest.fn(() => Math.random());

test("random", () => {
  const val = random();
  expect(val).toBeGreaterThan(0);
  expect(random).toHaveBeenCalled();
  expect(random).toHaveBeenCalledTimes(1);
});

목 함수 속성

mock() 의 결과는 몇 가지 추가 속성으로 장식된 새 함수입니다.

ts
import { mock } from "bun:test";

const random = mock((multiplier: number) => multiplier * Math.random());

random(2);
random(10);

random.mock.calls;
// [[ 2 ], [ 10 ]]

random.mock.results;
//  [
//    { type: "return", value: 0.6533907460954099 },
//    { type: "return", value: 0.6452713933037312 }
//  ]

사용 가능한 속성 및 메서드

다음 속성과 메서드가 목 함수에 구현됩니다.

속성/메서드설명
mockFn.getMockName()목 이름을 반환합니다
mockFn.mock.calls각 호출에 대한 호출 인수 배열
mockFn.mock.results각 호출에 대한 반환 값 배열
mockFn.mock.instances각 호출에 대한 this 컨텍스트 배열
mockFn.mock.contexts각 호출에 대한 this 컨텍스트 배열
mockFn.mock.lastCall가장 최근 호출의 인수
mockFn.mockClear()호출 기록을 지웁니다
mockFn.mockReset()호출 기록을 지우고 구현을 제거합니다
mockFn.mockRestore()원래 구현으로 복원합니다
mockFn.mockImplementation(fn)새 구현으로 설정합니다
mockFn.mockImplementationOnce(fn)다음 호출에만 대한 구현으로 설정합니다
mockFn.mockName(name)목 이름을 설정합니다
mockFn.mockReturnThis()반환 값을 this 로 설정합니다
mockFn.mockReturnValue(value)반환 값을 설정합니다
mockFn.mockReturnValueOnce(value)다음 호출에만 대한 반환 값을 설정합니다
mockFn.mockResolvedValue(value)해결된 Promise 값을 설정합니다
mockFn.mockResolvedValueOnce(value)다음 호출에만 대한 해결된 Promise 를 설정합니다
mockFn.mockRejectedValue(value)거부된 Promise 값을 설정합니다
mockFn.mockRejectedValueOnce(value)다음 호출에만 대한 거부된 Promise 를 설정합니다
mockFn.withImplementation(fn, callback)일시적으로 구현을 변경합니다

실제 예시

기본 목 사용

ts
import { test, expect, mock } from "bun:test";

test("목 함수 동작", () => {
  const mockFn = mock((x: number) => x * 2);

  // 목 호출
  const result1 = mockFn(5);
  const result2 = mockFn(10);

  // 호출 확인
  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith(5);
  expect(mockFn).toHaveBeenLastCalledWith(10);

  // 결과 확인
  expect(result1).toBe(10);
  expect(result2).toBe(20);

  // 호출 기록 검사
  expect(mockFn.mock.calls).toEqual([[5], [10]]);
  expect(mockFn.mock.results).toEqual([
    { type: "return", value: 10 },
    { type: "return", value: 20 },
  ]);
});

동적 목 구현

ts
import { test, expect, mock } from "bun:test";

test("동적 목 구현", () => {
  const mockFn = mock();

  // 다른 구현 설정
  mockFn.mockImplementationOnce(() => "first");
  mockFn.mockImplementationOnce(() => "second");
  mockFn.mockImplementation(() => "default");

  expect(mockFn()).toBe("first");
  expect(mockFn()).toBe("second");
  expect(mockFn()).toBe("default");
  expect(mockFn()).toBe("default"); // 기본 구현 사용
});

비동기 목

ts
import { test, expect, mock } from "bun:test";

test("비동기 목 함수", async () => {
  const asyncMock = mock();

  // 해결된 값 목
  asyncMock.mockResolvedValueOnce("first result");
  asyncMock.mockResolvedValue("default result");

  expect(await asyncMock()).toBe("first result");
  expect(await asyncMock()).toBe("default result");

  // 거부된 값 목
  const rejectMock = mock();
  rejectMock.mockRejectedValue(new Error("Mock error"));

  await expect(rejectMock()).rejects.toThrow("Mock error");
});

spyOn() 으로 스파이

목을 대체하지 않고 함수 호출을 추적할 수 있습니다. spyOn() 을 사용하여 스파이를 생성하세요. 이러한 스파이는 .toHaveBeenCalled().toHaveBeenCalledTimes() 에 전달될 수 있습니다.

ts
import { test, expect, spyOn } from "bun:test";

const ringo = {
  name: "Ringo",
  sayHi() {
    console.log(`Hello I'm ${this.name}`);
  },
};

const spy = spyOn(ringo, "sayHi");

test("spyon", () => {
  expect(spy).toHaveBeenCalledTimes(0);
  ringo.sayHi();
  expect(spy).toHaveBeenCalledTimes(1);
});

고급 스파이 사용

ts
import { test, expect, spyOn, afterEach } from "bun:test";

class UserService {
  async getUser(id: string) {
    // 원래 구현
    return { id, name: `User ${id}` };
  }

  async saveUser(user: any) {
    // 원래 구현
    return { ...user, saved: true };
  }
}

const userService = new UserService();

afterEach(() => {
  // 각 테스트 후 모든 스파이 복원
  jest.restoreAllMocks();
});

test("서비스 메서드 스파이", async () => {
  // 구현 변경 없이 스파이
  const getUserSpy = spyOn(userService, "getUser");
  const saveUserSpy = spyOn(userService, "saveUser");

  // 서비스 정상 사용
  const user = await userService.getUser("123");
  await userService.saveUser(user);

  // 호출 확인
  expect(getUserSpy).toHaveBeenCalledWith("123");
  expect(saveUserSpy).toHaveBeenCalledWith(user);
});

test("목 구현으로 스파이", async () => {
  // 스파이 및 구현 재정의
  const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
    id: "123",
    name: "Mocked User",
  });

  const result = await userService.getUser("123");

  expect(result.name).toBe("Mocked User");
  expect(getUserSpy).toHaveBeenCalledWith("123");
});

mock.module() 로 모듈 목

모듈 목킹은 모듈의 동작을 재정의할 수 있게 합니다. mock.module(path: string, callback: () => Object) 를 사용하여 모듈을 목하세요.

ts
import { test, expect, mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});

test("mock.module", async () => {
  const esm = await import("./module");
  expect(esm.foo).toBe("bar");

  const cjs = require("./module");
  expect(cjs.foo).toBe("bar");
});

Bun 의 나머지 부분과 마찬가지로 모듈 목은 importrequire 를 모두 지원합니다.

이미 가져온 모듈 재정의

이미 가져온 모듈을 재정의해야 하는 경우 특별한 작업이 필요하지 않습니다. mock.module() 을 호출하기만 하면 모듈이 재정의됩니다.

ts
import { test, expect, mock } from "bun:test";

// 목하려는 모듈이 여기에 있습니다.
import { foo } from "./module";

test("mock.module", async () => {
  const cjs = require("./module");
  expect(foo).toBe("bar");
  expect(cjs.foo).toBe("bar");

  // 여기서 업데이트합니다.
  mock.module("./module", () => {
    return {
      foo: "baz",
    };
  });

  // 그리고 라이브 바인딩이 업데이트됩니다.
  expect(foo).toBe("baz");

  // CJS 도 업데이트됩니다.
  expect(cjs.foo).toBe("baz");
});

호이스팅 및 프리로딩

모듈이 가져오기 전에 목되어야 하는 경우 --preload 를 사용하여 테스트 실행 전에 목을 로드해야 합니다.

ts
import { mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});
bash
bun test --preload ./my-preload

삶을 더 쉽게 하려면 bunfig.toml 에 preload 를 넣을 수 있습니다.

toml
[test]
# 테스트 실행 전에 이 모듈들을 로드합니다.
preload = ["./my-preload"]

모듈 목 모범 사례

프리로드 사용 시기

이미 가져온 모듈을 목하면 어떻게 되나요?

이미 가져온 모듈을 목하면 모듈은 모듈 캐시에서 업데이트됩니다. 이는 모듈을 가져오는 모든 모듈이 목킹된 버전을 얻지만 원래 모듈은 여전히 평가되었다는 것을 의미합니다. 즉 원래 모듈의 모든 사이드 이펙트가 여전히 발생했습니다.

원래 모듈이 평가되는 것을 방지하려면 --preload 를 사용하여 테스트 실행 전에 목을 로드해야 합니다.

실제 모듈 목 예시

ts
import { test, expect, mock, beforeEach } from "bun:test";

// API 클라이언트 모듈 목
mock.module("./api-client", () => ({
  fetchUser: mock(async (id: string) => ({ id, name: `User ${id}` })),
  createUser: mock(async (user: any) => ({ ...user, id: "new-id" })),
  updateUser: mock(async (id: string, user: any) => ({ ...user, id })),
}));

test("목킹된 API 와 사용자 서비스", async () => {
  const { fetchUser } = await import("./api-client");
  const { UserService } = await import("./user-service");

  const userService = new UserService();
  const user = await userService.getUser("123");

  expect(fetchUser).toHaveBeenCalledWith("123");
  expect(user.name).toBe("User 123");
});

외부 종속성 목

ts
import { test, expect, mock } from "bun:test";

// 외부 데이터베이스 라이브러리 목
mock.module("pg", () => ({
  Client: mock(function () {
    return {
      connect: mock(async () => {}),
      query: mock(async (sql: string) => ({
        rows: [{ id: 1, name: "Test User" }],
      })),
      end: mock(async () => {}),
    };
  }),
}));

test("데이터베이스 작업", async () => {
  const { Database } = await import("./database");
  const db = new Database();

  const users = await db.getUsers();
  expect(users).toHaveLength(1);
  expect(users[0].name).toBe("Test User");
});

전역 목 함수

모든 목 초기화

모든 목 함수 상태 (호출, 결과 등) 를 재설정하되 원래 구현은 복원하지 않습니다.

ts
import { expect, mock, test } from "bun:test";

const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());

test("모든 목 초기화", () => {
  random1();
  random2();

  expect(random1).toHaveBeenCalledTimes(1);
  expect(random2).toHaveBeenCalledTimes(1);

  mock.clearAllMocks();

  expect(random1).toHaveBeenCalledTimes(0);
  expect(random2).toHaveBeenCalledTimes(0);

  // 참고: 구현은 유지됩니다
  expect(typeof random1()).toBe("number");
  expect(typeof random2()).toBe("number");
});

이는 모든 목의 .mock.calls, .mock.instances, .mock.contexts.mock.results 속성을 재설정하지만 mock.restore() 와는 달리 원래 구현을 복원하지 않습니다.

모든 목 복원

각 목을 mockFn.mockRestore() 로 개별적으로 복원하는 대신 mock.restore() 를 호출하여 한 번의 명령으로 모든 목을 복원하세요. 이렇게 하면 mock.module() 로 재정의된 모듈의 값은 재설정되지 않습니다.

ts
import { expect, mock, spyOn, test } from "bun:test";

import * as fooModule from "./foo.ts";
import * as barModule from "./bar.ts";
import * as bazModule from "./baz.ts";

test("foo, bar, baz", () => {
  const fooSpy = spyOn(fooModule, "foo");
  const barSpy = spyOn(barModule, "bar");
  const bazSpy = spyOn(bazModule, "baz");

  // 원래 값
  expect(fooSpy).toBe("foo");
  expect(barSpy).toBe("bar");
  expect(bazSpy).toBe("baz");

  // 목 구현
  fooSpy.mockImplementation(() => 42);
  barSpy.mockImplementation(() => 43);
  bazSpy.mockImplementation(() => 44);

  expect(fooSpy()).toBe(42);
  expect(barSpy()).toBe(43);
  expect(bazSpy()).toBe(44);

  // 모두 복원
  mock.restore();

  expect(fooSpy()).toBe("foo");
  expect(barSpy()).toBe("bar");
  expect(bazSpy()).toBe("baz");
});

mock.restore() 를 사용하면 각 테스트 파일의 afterEach 블록이나 테스트 프리로드 코드에 추가하여 테스트의 코드 양을 줄일 수 있습니다.

Vitest 호환

Vitest 용으로 작성된 테스트와의 호환성을 높이기 위해 Bun 은 Jest 목킹 API 의 일부에 대한 별칭으로 vi 전역 객체를 제공합니다.

ts
import { test, expect } from "bun:test";

// Vitest 와 유사하게 'vi' 별칭 사용
test("vitest 호환", () => {
  const mockFn = vi.fn(() => 42);

  mockFn();
  expect(mockFn).toHaveBeenCalled();

  // 다음 함수들은 vi 객체에서 사용 가능합니다.
  // vi.fn
  // vi.spyOn
  // vi.mock
  // vi.restoreAllMocks
  // vi.clearAllMocks
});

이를 통해 모든 목을 다시 작성하지 않고도 Vitest 에서 Bun 으로 테스트를 이전하기 쉽습니다.

구현 세부정보

mock.module() 이 어떻게 작동하는지 이해하면 더 효과적으로 사용하는 데 도움이 됩니다.

캐시 상호작용

모듈 목은 ESM 과 CommonJS 모듈 캐시 모두와 상호작용합니다.

지연 평가

목 팩토리 콜백은 모듈이 실제로 가져오거나 요구될 때만 평가됩니다.

경로 확인

Bun 은 가져오기를 수행하는 것처럼 모듈 지정자를 자동으로 확인하며 다음을 지원합니다.

  • 상대 경로 ('./module')
  • 절대 경로 ('/path/to/module')
  • 패키지 이름 ('lodash')

가져오기 타이밍 효과

  • 첫 가져오기 전에 목킹하는 경우: 원래 모듈에서 사이드 이펙트가 발생하지 않습니다
  • 가져오기 후에 목킹하는 경우: 원래 모듈의 사이드 이펙트가 이미 발생했습니다

이러한 이유로 사이드 이펙트를 방지해야 하는 목의 경우 --preload 사용이 권장됩니다.

라이브 바인딩

목킹된 ESM 모듈은 라이브 바인딩을 유지하므로 목을 변경하면 모든 기존 가져오기가 업데이트됩니다.

고급 패턴

팩토리 함수

ts
import { mock } from "bun:test";

function createMockUser(overrides = {}) {
  return {
    id: "mock-id",
    name: "Mock User",
    email: "mock@example.com",
    ...overrides,
  };
}

const mockUserService = {
  getUser: mock(async (id: string) => createMockUser({ id })),
  createUser: mock(async (data: any) => createMockUser(data)),
  updateUser: mock(async (id: string, data: any) => createMockUser({ id, ...data })),
};

조건부 목

ts
import { test, expect, mock } from "bun:test";

const shouldUseMockApi = process.env.NODE_ENV === "test";

if (shouldUseMockApi) {
  mock.module("./api", () => ({
    fetchData: mock(async () => ({ data: "mocked" })),
  }));
}

test("조건부 API 사용", async () => {
  const { fetchData } = await import("./api");
  const result = await fetchData();

  if (shouldUseMockApi) {
    expect(result.data).toBe("mocked");
  }
});

목 정리 패턴

ts
import { afterEach, beforeEach } from "bun:test";

beforeEach(() => {
  // 일반적인 목 설정
  mock.module("./logger", () => ({
    log: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
  }));
});

afterEach(() => {
  // 모든 목 정리
  mock.restore();
  mock.clearAllMocks();
});

모범 사례

목을 간단하게 유지

ts
// 좋음: 단순하고 집중된 목
const mockUserApi = {
  getUser: mock(async id => ({ id, name: "Test User" })),
};

// 피하기: 지나치게 복잡한 목 동작
const complexMock = mock(input => {
  if (input.type === "A") {
    return processTypeA(input);
  } else if (input.type === "B") {
    return processTypeB(input);
  }
  // ... 많은 복잡한 로직
});

타입 안전 목 사용

ts
interface UserService {
  getUser(id: string): Promise<User>;
  createUser(data: CreateUserData): Promise<User>;
}

const mockUserService: UserService = {
  getUser: mock(async (id: string) => ({ id, name: "Test User" })),
  createUser: mock(async data => ({ id: "new-id", ...data })),
};

목 동작 테스트

ts
test("서비스가 API 를 올바르게 호출", async () => {
  const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };

  const service = new UserService(mockApi);
  await service.getUser("123");

  // 목이 올바르게 호출되었는지 확인
  expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
  expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});

참고

자동 목

__mocks__ 디렉토리와 자동 목은 아직 지원되지 않습니다. 이것이 Bun 으로 전환하는 것을 막고 있다면 이슈를 제기 해주세요.

ESM 대 CommonJS

모듈 목은 ESM 과 CommonJS 모듈에 대해 다른 구현을 가집니다. ES 모듈의 경우 Bun 은 JavaScriptCore 에 패치를 추가하여 런타임에 내보내기 값을 재정의하고 라이브 바인딩을 재귀적으로 업데이트할 수 있습니다.

Bun by www.bunjs.com.cn 편집