본문으로 바로가기

https://jestjs.io/docs/mock-function-api

 

Mock Functions · Jest

Mock functions are also known as "spies", because they let you spy on the behavior of a function that is called indirectly by some other code, rather than only testing the output. You can create a mock function with jest.fn(). If no implementation is given

jestjs.io

 

 

What mocking is?

 

mocking?

unit test를 작성함에 있어 실제 DB를 건드리게 되고 이러한 테스트가 자주 발생하게 된다면 테스트 실행 속도도 느려질 뿐더러, 코드 자체의 완결성이 아닌 인프라의 영향으로 테스트 결과가 달라질 수 있습니다.  때문에 외부 모듈의 복잡한 작업이나 DB 관련 작업 코드들은 mocking하여 다루는 것이 일반적입니다.

 

spy?

 

기존에 존재하는 함수에 전달되는 args나 호출 횟수를 탐지하기 위해서 spy를 붙일 때가 있고,

더 나아가서 spy를 붙인 함수 자체를 실행시키지 않고 implemented된 mocking 함수를 덧붙일 때도 있습니다.

쉽게 생각해서, 여러분이 테스트하고 추적하고 싶은 함수에 spy를 붙여서 감시한다고 생각하세요.

 

(제 경험상으론) spy function은 주로 mock을 할 수 없을 때 사용합니다. 언제 사용할 수가 없을까요?

테스팅해야 하는 함수가 테스트할 다른 함수 내에서도 사용되어 mocking을 할 경우 사용되는 함수를 테스트할 수 없기 때문에 원본은 두고 spy 해야 할 때가 있습니다. 여기에서 처럼요 https://darrengwon.tistory.com/1047

 

 

mocking

 

우선 mocking 함수를 만들어보도록하겠습니다.

고정적인 값을 반환하도록 만들 수도 있고, implemente 하여서 동적인 값을 반환하도록 만들 수도 있습니다. 간단!

// 고정된 값을 반환하는 mocking function
const mockedFunc = jest.fn();
mockedFunc.mockReturnValue(2);

// implemented한 mocking function. 동적임. 아래 둘 함수는 완전히 같은 기능을 하는 mocking function임.
const implementedMockedFunc = jest.fn().mockImplementation(a => a + 1);
const shorthandImpledmentedMockedFunc = jest.fn(a => a + 1);

 

이제 mocking 함수를 이용하여 테스트 코드를 작성해보겠습니다.

1. jest.fn()으로 모킹 함수를 만들고,

2. 모킹 함수가 반환해야 하는 값을 mockReturnValue, mockResolvedValue(비동기) 로 설정한 다음

3. mocking 함수를 이용하여 테스트 코드를 작성하면 됩니다.

const mockRepository = {
  findUser: jest.fn(),
  asyncFindUser: jest.fn(),
};

const obj = { id: 1, userName: 'darren' };

// 값 반환
mockRepository.findUser.mockReturnValue(obj);

// 비동기 값 반환
mockRepository.asyncFindUser.mockResolvedValue(obj);

describe('testing mocking func', () => {
  test('should return obj', () => {
    return expect(mockRepository.findUser()).toEqual(obj);
  });
  test('should return async obj', async () => {
    return expect(mockRepository.asyncFindUser()).resolves.toEqual(obj);
  });
});

 

기본적은 위와 같은데, 조금 독특하게 mock function이 값을 반환하게 만들 수도 있습니다.

예를 들어 mockReturnValueOnce를 chain으로 연달아 mock function에 달아줘서 호출 횟수에 따라 다른 값을 반환하게 할 수도 있습니다.

const mockedFunc = jest.fn();

// 첫 호출에 true, 두번 째 호출에 false, 이후 호출 undefined
mockedFunc.mockReturnValueOnce(true).mockReturnValueOnce(false);

console.log(mockedFunc()); // true
console.log(mockedFunc()); // false
console.log(mockedFunc()); // undefined
const mockedFunc = jest.fn();

// 첫 호출에 10, 두번 째 호출에 x, 이후 호출부터 계속 true 반환
mockedFunc.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(mockedFunc()); // 10
console.log(mockedFunc()); // x
console.log(mockedFunc()); // true
console.log(mockedFunc()); // true ...

 

 

mocking function에서 자주 사용하는 matcher

 

 

toBeCalledWith(a.k.a. toHaveBeenCalledWith)

정해진 인자가 mocking function에게 주어졌는지 테스트합니다.

Use .toHaveBeenCalledWith to ensure that a mock function was called with specific arguments.

 

toBeCalledTimes(a. k. a. toHaveBeenCalledTimes)

mocking function이 몇 번 호출되었는지 테스트합니다.

Use .toHaveBeenCalledTimes to ensure that a mock function got called exact number of times.

 

아래와 같은 꼴로 mock function에 주어진 인자와 호출 횟수를 테스트할 수 있습니다.

여기서 주의할 점은, 함수를 실행하지 않고, 함수 그 자체를 expect한다는 것입니다.

호출하면 값을 반환하잖아요? 그런데 우리는 그 값을 테스트하는 것이 아니라 함수를 테스트해보고 싶은 거니깐요.

상식적인건데, 이상하게 저를 포함해서 자주 실수하곤 하는 부분입니다. 

const mockRepository = {
  searchUser: jest.fn(),
  createUser: jest.fn(),
};

const obj = { id: 1, userName: 'darren' };

// 모두 비동기적인 값 반환
mockRepository.searchUser.mockResolvedValue(obj);
mockRepository.createUser.mockResolvedValue(obj);

describe('testing mocking func', () => {
  test('should called with args string name', () => {
    mockRepository.searchUser('darren');
    // mockRepository.searchUser()로 반환되는 값이 아니라 mockRepository.searchUser란 함수를 테스트
    return expect(mockRepository.searchUser).toBeCalledWith('darren');
  });

  test('should called once', () => {
    mockRepository.searchUser('darren');
    return expect(mockRepository.searchUser).toBeCalledTimes(1);
  });
});

 

 

외부 패키지 mocking하기

단순히 외부 패키지의 하나의 메서드만 mocking하는 방법도 있고, 전체를 전부 mocking할 수도 있습니다.

아래 코드는 axios 외부 패키지를 전부 mocking한 것입니다.

 

import 'regenerator-runtime/runtime';
import axios from 'axios';

// axios 전체를 mocking합니다.
jest.mock('axios');

async function findUser(id) {
  const response = await axios.get(`http://api.myweb.com/users/${id}`);
  return response;
}

const obj = { data: { name: 'darren' } };

test('should get user by id', async () => {
  axios.get.mockResolvedValue(obj); // axios의 get 메서드가 obj를 프로미스로 반환하도록 함
  const userData = await findUser(123);
  
  expect(userData).toHaveProperty('data');
  expect(userData.data).toHaveProperty('name', 'darren');
  expect(userData).toEqual(obj);
});

 

만약, axios의 get 메서드만 mocking하고 싶다면 아래처럼 해주면 됩니다.

jest.mock('axios', () => {
  return {
    get: jest.fn(),
  };
});

const obj = { data: { name: 'darren' } };

axios.get.mockResolvedValue(obj);

 

spy

https://jestjs.io/docs/jest-object#jestspyonobject-methodname

 

기존에 존재하는 함수에 전달되는 args나 호출 횟수를 탐지하기 위해서 spy를 붙일 때가 있고,

더 나아가서 spy를 붙인 함수 자체를 실행시키지 않고 implemented된 mocking 함수를 덧붙일 때도 있습니다.

쉽게 생각해서, 여러분이 테스트하고 추적하고 싶은 함수에 spy를 붙여서 감시한다고 생각하면 쉽습니다.

 

Creates a mock function similar to jest.fn but also tracks calls to object[methodName]. Returns a Jest mock function.

By default, jest.spyOn also calls the spied method. This is different behavior from most other test libraries.

If you want to overwrite the original function, you can use

jest.spyOn(object, methodName).mockImplementation(() => customImplementation)
object[methodName] = jest.fn(() => customImplementation);

 

실제로 써보겠습니다.

이렇게 spy된 함수는 들어온 인자와 호출된 횟수 등을 추적할 수 있게 됩니다.

const video = {
  play() {
    return true;
  },
};

test('plays video', () => {
  const spy = jest.spyOn(video, 'play');
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled(); // toHaveBeenCalledTimes(1)과 같음
  expect(isPlaying).toBe(true);

  spy.mockRestore(); // Beware that mockFn.mockRestore only works when the mock was created with jest.spyOn
});

 

여기에 추가적으로 spy된 함수에 mockImplementation을 통해서 함수 자체를 바꿀 수도 있습니다.

test('plays video', () => {
  const spy = jest.spyOn(video, 'play').mockImplementation(() => false);
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled(); // toHaveBeenCalledTimes(1)과 같음
  expect(isPlaying).toBe(false);

  spy.mockRestore(); // Beware that mockFn.mockRestore only works when the mock was created with jest.spyOn
});

console.log(video.play()); // true를 반환. mockRestore 되었기 때문.

 

 

etc

 

nest에서 mocking 테스트 관련 포스트를 작성한 바 있으니 이를 추가적으로 참고해보는 것도 좋을 것 같습니다.

https://darrengwon.tistory.com/999?category=915252 

https://darrengwon.tistory.com/1004?category=915252 

https://darrengwon.tistory.com/1046?category=915252 

https://darrengwon.tistory.com/1047?category=915252 

 

 

 

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체