Jest 사용법 (7) - 모의 함수 (Mock Functions)

modolee·2020년 9월 27일
8
post-thumbnail

Mock Functions 개요

  • 실제 구현 된 함수를 Mock(모의) 함수로 대체하면 코드 간의 상호 작용이 쉽게 테스트 가능
  • Mock 함수의 장점
    • 호출 시 함수에 전달되는 인자 캡쳐 가능
    • new를 통해서 생성할 때 인스턴스 캡쳐 가능
    • 테스팅 과정 중 return 값 설정 가능
  • Mock 함수의 2가지 사용 방법
    • 테스트 코드에 사용하기 위해 mock 함수 생성
    • module dependency를 override하기 위해 manual mock 작성

Mock function 사용

  • 함수를 구현하고 해당 함수를 테스트하기 위해 mock 함수를 사용

예제 - 직접 구현 한 forEach 함수를 테스트

구현

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

테스트

  • mock 함수 호출 횟수 체크
  • mock 함수 호출 시 넘겨 받은 인자 값 체크
  • mock 함수 호출의 결과 값 체크
describe('Test self implemented forEach function', () => {
  const mockCallback = jest.fn(x => 42 + x);
  forEach([0, 1], mockCallback);

  test('The mock function is called twice', () => {
    expect(mockCallback.mock.calls.length).toBe(2);
  });

  test('The first argument of the first call to the function was 0', () => {
    expect(mockCallback.mock.calls[0][0]).toBe(0);
  });

  test('The first argument of the second call to the function was 1', () => {
    expect(mockCallback.mock.calls[1][0]).toBe(1);
  });

  test('The return value of the first call to the function was 42', () => {
    expect(mockCallback.mock.results[0].value).toBe(42);
  });
});

.mock property

  • 모든 mock 함수는 .mock 이라는 프로퍼티를 가지고 있음
  • 해당 프로퍼티는 mock 함수가 어떻게 호출 되었는지, 어떤 값을 return 했는지 등의 정보를 가지고 있음
  • 매 호출 시 this 값도 가지고 있음
  • mock.instances는 mock 함수로 부터 new를 사용해 인스턴스화 된 객체를 배열로 가지고 있음
describe('mock property', () => {
  const myMock = jest.fn(function (_name) {
    this.name = _name;
  });

  test('track the value of this for each call', () => {

    const a = new myMock('a function');
    const b = {};
    const bound = myMock.bind(b);
    bound('b function');

    expect(myMock.mock.instances[0]).toEqual(a);
    expect(myMock.mock.instances[1]).toEqual(b);
  });
})

Mock Return Values

  • mock 함수의 return 값을 테스트 중간에 주입(inject) 가능
describe('test mock return values', () => {
    const myMock = jest.fn();
    expect(myMock()).toEqual(undefined);

    myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

    test('test first call', () => {
      expect(myMock()).toBe(10);
    });

    test('test second call', () => {
      expect(myMock()).toBe('x');
    });

    test('test third call', () => {
      expect(myMock()).toBeTruthy();
    });

    test('test fourth call', () => {
      expect(myMock()).toBeTruthy();
    });

  });
  • 연속적으로 값을 전달하는 형태의 코드에서 효율적으로 사용 가능
  • 복잡하게 실제 함수를 수정할 필요 없이, 직관적으로 값을 지정할 수 있음
  • mock.calls는 mock 함수 호출 시 전달 된 인자 값들을 담고 있는 배열
describe('continuation-passing style', () => {
    const filterTestFn = jest.fn();
    filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

    const result = [11, 12].filter(num => filterTestFn(num));

    test('test result', () => {
      expect(result).toEqual([11]);
    });

    test('test call arguments', () => {
      expect(filterTestFn.mock.calls).toEqual([[11], [12]]);
    });

  });

Mocking Modules

  • 모듈 내부의 특정 메소드의 동작을 모의로 만듦
  • mock async function을 만들 때 유용하게 사용

실제 구현 된 API 호출 모듈

// src/users.js
import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;

mock으로 구현 된 모듈을 이용한 테스트

import axios from 'axios';
import Users from './users';

jest.mock('axios');

describe('Mocking Modules', () => {
  test('should fetch users', async () => {
    const users = [{name: 'Bob'}];
    const resp = {data: users};
    axios.get.mockResolvedValue(resp);
		// 아래 코드와 동일하게 동작
    // axios.get.mockImplementation(() => Promise.resolve(resp))

    const result = await Users.all();
    expect(result).toEqual(users);
  });
});

Mock Implementations

함수 전체를 대체

  • 아직 구현이 안되어 있거나 내부 구현이 복잡하여 테스트 하기 힘든 경우
  • 테스트를 위해 간단한 구현체를 만들어서 해당 함수를 대체
  • jest.fn, mockImeplementation을 사용하여 구현
  • jest.mock으로 mocking 하겠다고 지정하게 되면, 실제 구현체를 가려지고 mock 구현체를 바라보게 됨
// src/foo.js
export default () => {
  console.log('Actual implementation');
  return 42;
}
jest.mock('../src/foo'); // 해당 모듈을 자동으로 mock 모듈로 취급 됨
import foo from '../src/foo';

describe('Mock Implementations', () => {
  test('test jest fn', () => {
    const myMockFn = jest.fn(cb => cb(null, true));

    expect(myMockFn((err, val) => val)).toBeTruthy();
  });

  test('test mock implementation', () => {
    foo.mockImplementation(() => {
      console.log('Mock implementation');
      return 77;
    });

    expect(foo()).not.toBe(42); // Actual implementation이 아닌,
    expect(foo()).toBe(77);     // Mock implementation이 실행 됨
  });
});

한 번의 호출에 대한 구현만 대체

  • 다양한 경우를 테스트 하는 경우
  • 호출 마다 다른 결과 값을 return 하도록 구현 가능
  • mockImplementationOnce로 구현
  • 한 번의 호출에 대한 구현을 지정할 수 있고, 지정 된 구현이 더 이상 없다면 jest.fn에 지정 한 기본 구현을 사용. 기본 구현이 없다면 undefined return
describe('Mock Implementation Once', () => {
  test('test mockImplementationOnce without default', () => {
    const myMockFn = jest
      .fn()
      .mockImplementationOnce(cb => cb(null, true))
      .mockImplementationOnce(cb => cb(null, false));

    const cbFn = (err, val) => val;

    expect(myMockFn(cbFn)).toBeTruthy();
    expect(myMockFn(cbFn)).toBeFalsy();
    expect(myMockFn(cbFn)).toBeUndefined();
  });

  test('test mockImplementationOnce with default', () => {
    const myMockFn = jest
      .fn(cb => cb(null, 'default'))
      .mockImplementationOnce(cb => cb(null, true))
      .mockImplementationOnce(cb => cb(null, false));

    const cbFn = (err, val) => val;

    expect(myMockFn(cbFn)).toBeTruthy();
    expect(myMockFn(cbFn)).toBeFalsy();
    expect(myMockFn(cbFn)).toBe('default');
  });
});

체이닝을 위해 this를 return하는 경우

describe('Mock Return This', () => {
  test('test mockReturnThis', () => {
    const myObj = {
      myMethod: jest.fn().mockReturnThis(),
      // 아래 구현과 동일하게 동작
      // myMethod: jest.fn(function () {
      //         return this;
      //       }),
      log: jest.fn(() => 'logging')
    };

    expect(myObj.myMethod()).toEqual(myObj);
    expect(myObj.myMethod().log()).toBe('logging');
  });
});

Mock Names

  • mock 함수에 이름을 지정하면 에러 발생 시 좀 더 명확하게 알 수 있음
describe('Mock Names', () => {
  test('test display mock function name', () => {
    const myMockFn = jest
      .fn()
      .mockName('namedMockFn')
    // myMockFn(1);
    expect(myMockFn).toHaveBeenCalled(); // 호출이 되는지 테스트
  });
});

// 이름 지정 안한 경우
// Error: expect(jest.fn()).toHaveBeenCalled()

// 이름 지정 한 경우
// Error: expect(namedMockFn).toHaveBeenCalled()

Custom Matchers

  • .mock 프로퍼티를 통해서 직접 구현이 가능하지만, 조금 더 사용하기 편하게 제공되는 matcher
describe('Custom Matchers', () => {
  const mockFunc = jest.fn();
  const arg1 = 1;
  const arg2 = 2;

  test('The mock function was called at least once', () => {
    mockFunc();
    expect(mockFunc).toHaveBeenCalled();
    // 아래 코드와 동일하게 동작
    // expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
  });

  test('The mock function was called at least once with the specified args', () => {
    mockFunc(arg1, arg2);

    expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
    // 아래 코드와 동일하게 동작
    // expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
  });

  test('The last call to the mock function was called with the specified args', () => {
    mockFunc(3, 4);
    mockFunc(arg1, arg2);

    expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
    // 아래 코드와 동일하게 동작
    // expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
    //   arg1,
    //   arg2,
    // ]);
  });

  test('All calls and the name of the mock is written as a snapshot', () => {
    mockFunc(arg1, arg2);
    expect(mockFunc).toMatchSnapshot();
    // 아래 코드와 동일하게 동작
    // expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
    // expect(mockFunc.getMockName()).toBe('a mock name');
  });

});
profile
기초가 탄탄한 백엔드 개발자를 꿈꿉니다.

0개의 댓글