λͺ¨ν‚Ή(Mocking)

@bbearcookie Β· May 21, 2023 Β· 9 min read

λͺ¨ν‚Ήμ΄λž€?

κΈ°μ‘΄ ν•¨μˆ˜μ˜ κΈ°λŠ₯을 ν‰λ‚΄λ‚΄λŠ” κ°€μ§œ ν•¨μˆ˜λ₯Ό λ§Œλ“€μ–΄ λ‚΄λŠ” 것을 λ§ν•œλ‹€.

μš°λ¦¬κ°€ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μž‘μ„±ν•  λ•Œ λ°μ΄ν„°λ² μ΄μŠ€λ‚˜ μ™ΈλΆ€μ˜ μ„œλ²„λ‘œλΆ€ν„° 데이터λ₯Ό κ°€μ Έμ˜€λŠ” λ“±μ˜ 외뢀에 μ˜μ‘΄ν•΄μ•Ό ν•˜λŠ” κ²½μš°κ°€ μžˆλ‹€. 이런 κ²½μš°μ—λŠ” λ‹¨μœ„ ν…ŒμŠ€νŠΈκ°€ μ™ΈλΆ€μ˜ 상황에 λ”°λΌμ„œ κ²°κ³Όκ°€ λ‹¬λΌμ§€λŠ” 문제점이 λ°œμƒν•˜κΈ° λ•Œλ¬Έμ— 외뢀에 μ˜μ‘΄ν•˜λŠ” μ½”λ“œλ₯Ό κ°€μ§œλ‘œ λŒ€μ²΄ν•˜λŠ” λͺ¨ν‚Ήμ„ ν™œμš©ν•  수 μžˆλ‹€.

Jest μ—μ„œ λͺ¨ν‚Ήμ„ ν™œμš©ν•˜λŠ” 방법은 jest.fn(), jest.spyOn(), jest.mock() 이 μžˆλ‹€.

μ‹€ν–‰ ν™˜κ²½ 좔적

λͺ¨ν‚Ή ν•¨μˆ˜λŠ” λ‚΄λΆ€μ μœΌλ‘œ .mock ν”„λ‘œνΌν‹°λ₯Ό κ°€μ§€κ³  μžˆλ‹€.

이 ν”„λ‘œνΌν‹°μ— λ“€μ–΄μžˆλŠ” μ—¬λŸ¬ 속성듀을 ν†΅ν•΄μ„œ λͺ¨ν‚Ή ν•¨μˆ˜κ°€ λͺ‡ 번 호좜 λ˜μ—ˆλŠ”μ§€, μ–΄λ–€ 값을 인자둜 전달 λ°›μ•˜λŠ”μ§€, this의 값은 λ¬΄μ—‡μ΄μ—ˆλŠ”μ§€ λ“±μ˜ λ‹€μ–‘ν•œ 정보λ₯Ό 좔적할 수 μžˆλ‹€.

mockFn.mock.calls

λͺ¨ν‚Ή ν•¨μˆ˜μ— μ „λ‹¬ν–ˆλ˜ 인자의 정보λ₯Ό κΈ°μ–΅ν•˜λŠ” λ°°μ—΄ λ³€μˆ˜μ΄λ‹€. λ§Œμ•½ λͺ¨ν‚Ή ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•  λ•Œ f('arg1', 'arg2'), f('arg3', 'arg4') 의 μˆœμ„œλ‘œ μ§„ν–‰ν–ˆλ‹€λ©΄ λ‚΄λΆ€μ˜ λ‚΄μš©μ€ λ‹€μŒκ³Ό κ°™λ‹€:

[
  ['arg1', 'arg2'],
  ['arg3', 'arg4'],
];

mockFn.mock.results

λͺ¨ν‚Ή ν•¨μˆ˜κ°€ λ°˜ν™˜ν–ˆλ˜ 값을 κΈ°μ–΅ν•˜λŠ” 객체가 λ‹΄κΈ΄ λ°°μ—΄ λ³€μˆ˜μ΄λ‹€. 객체 λ‚΄λΆ€μ—λŠ” type κ³Ό value 값을 ν¬ν•¨ν•˜κ³  μžˆλ‹€.

Type μ’…λ₯˜

  • return: λͺ¨ν‚Ή ν•¨μˆ˜κ°€ 값을 μ •μƒμ μœΌλ‘œ λ°˜ν™˜ν–ˆλ˜ κ²½μš°μ΄λ‹€.
  • throw: λͺ¨ν‚Ή ν•¨μˆ˜κ°€ μ˜ˆμ™Έλ₯Ό 던쑌던 κ²½μš°μ΄λ‹€.
  • imcomplete: 아직 ν˜ΈμΆœμ— λŒ€ν•œ 싀행이 μ™„λ£Œλ˜μ§€ μ•Šμ€ κ²½μš°μ΄λ‹€.
[
  {
    type: 'return',
    value: 'result1',
  },
  {
    type: 'throw',
    value: {
      /* Error instance */
    },
  },
  {
    type: 'return',
    value: 'result2',
  },
];

mockFn.mock.instances

λͺ¨ν‚Ή ν•¨μˆ˜κ°€ new μ—°μ‚°μžλ‘œ μΈμŠ€ν„΄μŠ€ν™” 된 κ²½μš°μ— ν•΄λ‹Ή 객체의 μΈμŠ€ν„΄μŠ€κ°€ λ‹΄κΈ΄ λ°°μ—΄ λ³€μˆ˜μ΄λ‹€.

const mockFn = jest.fn();

const a = new mockFn();
const b = new mockFn();

mockFn.mock.instances[0] === a; // true
mockFn.mock.instances[1] === b; // true

mockFn.mock.contexts

λͺ¨ν‚Ή ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν–ˆλ˜ λ‹Ήμ‹œμ— this 에 λ°”μΈλ”©λ˜λŠ” 객체 정보가 λ‹΄κΈ΄ λ°°μ—΄ λ³€μˆ˜μ΄λ‹€.

describe('mock.contexts 에 λŒ€ν•œ μ½”λ“œ', () => {
  const mockFn = jest.fn();

  class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
      this.name = name;
      this.age = age;
    }

    callMockFn(...args: any[]) {
      mockFn.call(this, ...args);
    }
  }

  const kim = new Person('kim', 20)
  kim.callMockFn('a', 'b');
  kim.callMockFn(10, 20);

  const lee = new Person('lee', 25);
  lee.callMockFn('a', 'b');
  lee.callMockFn(10, 20);

  test('ν˜ΈμΆœν–ˆλ˜ λ‹Ήμ‹œμ˜ this κ°’ ν…ŒμŠ€νŠΈ', () => {
    expect(mockFn.mock.contexts[0]).toStrictEqual(kim); // true
    expect(mockFn.mock.contexts[1]).toStrictEqual(kim); // true
    expect(mockFn.mock.contexts[2]).toStrictEqual(lee); // true
    expect(mockFn.mock.contexts[3]).toStrictEqual(lee); // true
  });
});
// mockFn.mock.contexts
[
  Person { name: 'kim', age: 20 },
  Person { name: 'kim', age: 20 },
  Person { name: 'lee', age: 25 },
  Person { name: 'lee', age: 25 }
]

μœ μš©ν•œ Matcher

λͺ¨ν‚Ή ν•¨μˆ˜μ— λŒ€ν•œ μ‹€ν–‰ ν™˜κ²½μ„ μΆ”μ ν•˜κΈ° μœ„ν•΄μ„œ .mock ν”„λ‘œνΌν‹°λ₯Ό 직접 μ°Έμ‘°ν•˜μ§€ μ•Šκ³  expect() ν•¨μˆ˜μ˜ Matcher λ₯Ό μ΄μš©ν•˜λŠ” 방법도 μžˆλ‹€.

  • .toHaveBeenCalled()
    ν•¨μˆ˜κ°€ 호좜된 적이 μžˆλŠ”μ§€λ₯Ό ν™•μΈν•œλ‹€.
    .toBeCalled() 둜 μ‚¬μš©ν•  μˆ˜λ„ μžˆλ‹€.
  • .toHaveBeenCalledTimes(number)
    ν•¨μˆ˜κ°€ νŠΉμ • 횟수만큼 ν˜ΈμΆœλ˜μ—ˆλŠ”μ§€λ₯Ό ν™•μΈν•œλ‹€.
    .toBeCalledTimes(number) 둜 μ‚¬μš©ν•  μˆ˜λ„ μžˆλ‹€.
  • .toHaveBeenCalledWith(arg1, arg2, ...)
    ν•¨μˆ˜κ°€ ν•΄λ‹Ή 인수λ₯Ό 전달받고 ν˜ΈμΆœλ˜μ—ˆλŠ”μ§€λ₯Ό ν™•μΈν•œλ‹€.
    .toBeCalledWith() 둜 μ‚¬μš©ν•  μˆ˜λ„ μžˆλ‹€.
  • .toHaveReturned()
    ν•¨μˆ˜κ°€ 값을 λ°˜ν™˜ν•œ 적이 μžˆλŠ”μ§€λ₯Ό ν™•μΈν•œλ‹€.
    .toReturn() 둜 μ‚¬μš©ν•  μˆ˜λ„ μžˆλ‹€.
  • .toHaveReturnedTimes(number)
    ν•¨μˆ˜κ°€ νŠΉμ • 횟수만큼 λ°˜ν™˜λ˜μ—ˆλŠ”μ§€λ₯Ό ν™•μΈν•œλ‹€.
    .toReturnTimes(number) 둜 μ‚¬μš©ν•  μˆ˜λ„ μžˆλ‹€.
  • .toHaveReturnedWith(value)
    ν•¨μˆ˜κ°€ ν•΄λ‹Ή 값을 λ°˜ν™˜ν•œ 적이 μžˆλŠ”μ§€λ₯Ό ν™•μΈν•œλ‹€.
    .toReturnWith(value) 둜 μ‚¬μš©ν•  μˆ˜λ„ μžˆλ‹€.

jest.fn()

jest.fn() 을 ν˜ΈμΆœν•˜λ©΄ λͺ¨ν‚Ή ν•¨μˆ˜κ°€ μƒμ„±λœλ‹€.
λͺ¨ν‚Ή ν•¨μˆ˜λ₯Ό μƒμ„±ν•œ λ’€μ—λŠ” κ·Έ ν•¨μˆ˜κ°€ μ–΄λ–€ 값을 λ°˜ν™˜ν•  것인지λ₯Ό μ •μ˜ν•΄μ€˜μ•Ό ν•˜λŠ”λ° λ‹€μŒκ³Ό 같은 APIκ°€ μ‘΄μž¬ν•œλ‹€:

  • mockFn.mockReturnValue(value)
    λͺ¨ν‚Ή ν•¨μˆ˜κ°€ value λ₯Ό λ°˜ν™˜ν•˜λ„λ‘ ν•œλ‹€.
  • mockFn.mockImplementation(fn)
    λͺ¨ν‚Ή ν•¨μˆ˜μ˜ λ‚΄λΆ€μ—μ„œ λ™μž‘ν•˜λŠ” μ½”λ“œλ₯Ό 직접 μž‘μ„±ν•œλ‹€.
  • mockFn.mockResolvedValue(value)
    λͺ¨ν‚Ή ν•¨μˆ˜κ°€ ν•΄λ‹Ή value λ₯Ό μ΄ν–‰ν•˜λŠ” Promise λ₯Ό λ°˜ν™˜ν•œλ‹€.
  • mockFn.mockRejectedValue(value)
    λͺ¨ν‚Ή ν•¨μˆ˜κ°€ ν•΄λ‹Ή value λ₯Ό 이유둜 κ±°λΆ€ν•˜λŠ” Promise λ₯Ό λ°˜ν™˜ν•œλ‹€.

각각의 APIμ—λŠ” mockFn.mockReturnValueOnce(value) 와 같이 Once κ°€ μ ‘λ―Έμ‚¬λ‘œ 뢙은 것도 μ œκ³΅ν•˜λŠ”λ°, μ΄λŠ” μ΄ν›„μ˜ ν˜ΈμΆœμ— λŒ€ν•΄μ„œ ν•œ 번만 μž‘λ™ν•˜λŠ” API이닀.

μ˜ˆμ‹œ μ½”λ“œ

test('mocking', async () => {
  const mockFn = jest
    .fn()
    .mockReturnValue('default')
    .mockReturnValueOnce(5)
    .mockImplementationOnce(() => 'hello')
    .mockResolvedValueOnce('Resolved')
    .mockRejectedValueOnce(new Error('Rejected'));

  console.log(mockFn()); // 5
  console.log(mockFn()); // 'hello'
  console.log(mockFn()); // Promise { 'Resolved' }
  console.log(mockFn()); // Promise { <rejected> Error: Rejected }
  console.log(mockFn()); // 'default'

  expect(mockFn()).toBe('default');
});

μ œλ„€λ¦­

νƒ€μž…μŠ€ν¬λ¦½νŠΈλ₯Ό μ‚¬μš©ν•˜λ©΄ jest.fn() 에 μ œλ„€λ¦­ 인자λ₯Ό μ„Έ 개 λ„˜κ²¨μ€„ 수 μžˆλ‹€.

function fn<T, Y extends any[], C = any>(implementation?: (this: C, ...args: Y) => T): Mock<T, Y, C>;
  • T: ν•΄λ‹Ή λͺ¨ν‚Ή ν•¨μˆ˜κ°€ λ°˜ν™˜ν•˜λŠ” κ°’μ˜ νƒ€μž…μ„ μ§€μ •ν•œλ‹€.
  • Y: ν•΄λ‹Ή λͺ¨ν‚Ή ν•¨μˆ˜κ°€ λ°›λŠ” λ§€κ°œλ³€μˆ˜μ˜ νƒ€μž…μ„ μ§€μ •ν•œλ‹€.
  • C: μ •ν™•ν•˜κ²ŒλŠ” λͺ¨λ₯΄κ² μ§€λ§Œ, this 에 바인딩 λ˜λŠ” κ°’μ˜ νƒ€μž…μ„ μ§€μ •ν•˜λŠ” λ“― ν•˜λ‹€.

μ˜ˆμ‹œ μ½”λ“œ

λ‹€μŒμ€ number ν˜•νƒœμ˜ 인자 두 개λ₯Ό λ°›μ•„μ„œ 합을 λ°˜ν™˜ν•˜λŠ” μ½”λ“œμ΄λ‹€:

test('1 + 2 = 3', () => {
  const adder = jest.fn<number, [number, number]>((a, b) => a + b);
  expect(adder(1, 2)).toBe(3);
});

jest.spyOn()

μ‹€ν–‰ ν™˜κ²½μ„ μΆ”μ ν•˜λŠ” κΈ°λŠ₯은 λͺ¨ν‚Ή ν•¨μˆ˜μ—λ§Œ 적용이 λœλ‹€.

κ·Έλ ‡κΈ° λ•Œλ¬Έμ— μ‹€μ œ κ΅¬ν˜„ μ½”λ“œμ˜ ν•¨μˆ˜ μžμ²΄μ— λŒ€ν•΄μ„œλŠ” μ‹€ν–‰ ν™˜κ²½μ„ 좔적할 수 μ—†λŠ”λ°, κ°„ν˜Ή ν•¨μˆ˜μ˜ κΈ°λŠ₯을 ν‰λ‚΄λ‚΄λŠ” μƒˆλ‘œμš΄ κ°€μ§œ ν•¨μˆ˜λ₯Ό λ§Œλ“œλŠ” 것이 μ•„λ‹ˆλΌ 기쑴의 ν•¨μˆ˜λ₯Ό κ·ΈλŒ€λ‘œ μ‚¬μš©ν•œ μ±„λ‘œ μ‹€ν–‰ ν™˜κ²½λ§Œ μΆ”μ ν•˜κ³  싢은 κ²½μš°κ°€ μžˆλ‹€.

이런 κ²½μš°μ—λŠ” jest.spyOn() 으둜 기쑴에 μ‘΄μž¬ν•˜λŠ” 객체의 일뢀 λ©”μ†Œλ“œκ°€ ν˜ΈμΆœλ˜λŠ” ν™˜κ²½μ„ μ§€μΌœλ³΄λŠ” λͺ¨ν‚Ή ν•¨μˆ˜λ₯Ό λ§Œλ“€ 수 μžˆλ‹€.

μ˜ˆμ‹œ μ½”λ“œ

μ•„λž˜ μ½”λ“œλŠ” κΈ°μ‘΄ 객체 calculator 의 add λ©”μ†Œλ“œκ°€ λ™μž‘ν•˜λŠ” 것을 μ§€μΌœλ³΄λŠ” μ½”λ“œμ΄λ‹€.

describe('about spyon', () => {
  const calculator = {
    add: (a: number, b: number) => a + b,
    minus: (a: number, b: number) => a - b
  }

  test('toHaveReturnedWith', () => {
    const spy = jest.spyOn(calculator, 'add'); // calculator 객체의 add λ©”μ†Œλ“œμ˜ λ™μž‘μ„ μ§€μΌœλ³Έλ‹€.
    
    calculator.add(1, 2); // λͺ¨ν‚Ή ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•˜λŠ”κ²Œ μ•„λ‹ˆλΌ, 기쑴의 ν•¨μˆ˜λ₯Ό κ·ΈλŒ€λ‘œ μ‹€ν–‰ν•˜κ³  μžˆλ‹€.
    calculator.add(3, 2);
    calculator.add(5, 4);
    
    expect(spy).toHaveReturnedWith(3);
  });
});

jest.mock()

νŠΉμ • λͺ¨λ“ˆμ„ λͺ¨ν‚Ήν•˜λŠ” κΈ°λŠ₯이닀.

ν…ŒμŠ€νŠΈ ν•˜λ €λŠ” μ½”λ“œμ—μ„œ λͺ¨ν‚Ήν•΄μ•Ό ν•˜λŠ” 뢀뢄이 λ§Žλ‹€λ©΄ 일일히 jest.fn() 으둜 μƒˆλ‘œμš΄ λͺ¨ν‚Ή ν•¨μˆ˜λ₯Ό λ§Œλ“œλŠ” 과정도 ꡉμž₯히 λ²ˆκ±°λ‘œμ›Œμ§„λ‹€.

특히 axios 같이 μ™ΈλΆ€ λΌμ΄λΈŒλŸ¬λ¦¬μ— μ˜μ‘΄ν•˜λŠ” 뢀뢄에 λŒ€ν•œ λͺ¨ν‚Ήμ„ μ²˜λ¦¬ν•΄μ•Ό ν•˜λŠ” κ²½μš°μ— 어렀움이 λ”ν•΄μ§€λŠ”λ° jest.mock() 을 μ΄μš©ν•˜λ©΄ νŠΉμ • λͺ¨λ“ˆμ„ ν•œλ²ˆμ— λͺ¨ν‚Ήν•  수 μžˆλ‹€.

μ‚¬μš© 방법은 μ΅œμƒμœ„ 레벨의 μ˜μ—­μ—μ„œ jest.mock(μ‚¬μš©ν•  λͺ¨λ“ˆ) 을 ν˜ΈμΆœν•΄μ£Όλ©΄ λœλ‹€.

직접 λ§Œλ“  messageService 와 μ™ΈλΆ€ 라이브러리인 axios μ—μ„œ λ‚΄λ³΄λ‚΄λŠ” λͺ¨λ“  ν•¨μˆ˜μ— λŒ€ν•΄μ„œ λͺ¨ν‚Ήν•˜κ³  μ‹Άλ‹€λ©΄ λ‹€μŒκ³Ό 같이 μž‘μ„±ν•œλ‹€:

jest.mock('../messageService');
jest.mock('axios');

참고 자료

Mock Functions
Mock Functions API
jest.fn(), jest.spyOn() ν•¨μˆ˜ λͺ¨ν‚Ή (DaleSeo)
λͺ¨ν‚Ή Mocking 정리 - jest.fn / jest.mock / jest.spyOn (Inpa)

@bbearcookie
Frontend Developer