Jest 공식 문서 해석하며 공부하기 - Mock Functions

배지로·2022년 6월 25일
0
post-thumbnail

Mock Functions

Mock Function은 함수의 실제 구현을 지움으로써, 함수로의 호출(그리고 그 호출에서 전달되는 인자들)을 캡처링함으로써, new로 인스턴스화된 생성자 함수의 인스턴스를 캡처링함으로써, 리턴값의 test-time 환경설정을 허용함으로써 코드 간의 링크를 테스트해볼 수 있게 합니다.

mock function을 사용하기 위한 두가지 방법: 테스트 코드 내에서 mock function을 생성하거나, module depedency를 오버라이드하기 위하여 manual mock을 작성하는 방법이 있습니다.

Using a mock function

주어지는 배열 내 각 원소마다 콜백함수를 호출하는 forEach 함수의 구현을 테스팅한다고 상상해봅시다.

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

이 함수를 테스트하기 위해서, 우리는 mock 함수를 사용하여 의도한대로 콜백이 호출되고 있는지 mock의 state를 사용하여 검사할 수 있습니다.

const mockCallback=jest.fn(x=>42+x);
forEach([0,1], mockCallback);

// mock function이 2번 호출된다
expect(mockCallback.mock.calls.length).toBe(2);

//함수의 첫번째 호출의 첫번째 인자는 0이다
expect(mockCallback.mock.calls[0][0]).toBe(0);

//함수의 두번째 호출에 첫번째 인자는 1이다
expect(mockCallback.mock.calls[1][0]).toBe(1);

//함수의 첫번째 호출에 대한 리턴 값은 42이다
expect(mockCallback.mock.results[0].value).toBe(42);

.mock property

모든 mock function은 특별한 .mock 속성을 가지고 있습니다. 이 속성은 함수 호출 빈도와 함수의 리턴값 데이터를 가지고 있습니다. .mock 속성은 매 호출마다 this 값 또한 추적하는데, 이것은 검사를 잘 할 수 있도록 합니다.

const myMock1=jest.fn();
const a=new myMock();
console.log(myMock1.mock.instances);
// > [ <a> ]

const myMock2=jest.fn();
const b={};
const bound=myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
//> [ <b> ]

이 mock 멤버들은 이 함수들이 어떻게 호출되고 있는지, 인스턴스화했는지, 혹은 무엇을 반환하는지 확실하게 알아보기 위해 테스트할 때 매우 유용합니다.

//이 함수는 1번만 호출된다
expect(someMockFunction.mock.calls.length).toBe(1);

//첫번째 함수 호출의 첫번째 인자는 'first arg'이다
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

//첫번째 함수 호출의 두번째 인자는 'second arg'이다
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

//첫번째 함수 호출의 리턴 값은 'return value'이다
expect(someMockFunction.mock.results[0].value).toBe('return value');

//함수는 특정 'this' 컨텍스트에서 호출되었다. : element 객체에서
expect(someMockFunction.mock.contexts[0]).toBe(element);

//이 함수는 2번 인스턴스화했다
expect(someMockFunction.mock.instances.length).toBe(2);

//이 함수에서 첫번째로 인스턴스화된 객체는 'name' 속성의 값이 'test'로 세팅되어 있다
expect(someMockFunction.mock.instances[0].name).toEqual('test');

//마지막 함수 호출의 첫번째 인자는 'test'이다
expect(someMockFunction.mock.lastCall[0]).toBe('test')

Mock Return Values

Mock함수는 테스트하는 동안에 당신의 코드에 테스트 값이 주입되어 사용되기도 합니다.

const myMock=jest.fn();
console.log(myMock());
// > undefined

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

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

Mock 함수는 함수형 컨티뉴에이션-패신 스타일(Continuation-passing style)을 사용하는 코드에서 굉장히 효과적입니다. 이 스타일로 쓰여진 코드는 진짜 컴포넌트의 동작을 재창조해야 하는 복잡한 스텁(stub)의 사용을 피할 수 있게 합니다. 즉, 값을 사용하기 바로 직전에 테스트에 주입하는 것입니다.

const filterTestFn=jest.fn();

//첫번째 호출에는 mock이 'true'를 반환하고 
//두번째 호출에는 'false'를 반환하도록 함
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

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

console.log(result);
//>[11]

console.log(filterTestFn.mock.calls[0][0]); //11
console.log(filterTestFn.mock.calls[1][0]); //12

대부분의 실제 예시에서는 의존적인 컴포넌트에서 mock function를 파악하여 구성하는 것을 포함하지만, 기술은 동일합니다. 이러한 경우에는 직접적으로 테스트되지 않는 함수 내의 로직 구현을 위한 시도는 피하세요.

Mocking Modules

API로부터 users 정보를 가져오는 클래스가 있다고 가정해봅시다. 클래스에서는 axios를 사용하여 API를 호출하며 users 정보를 포함하는 data 속성을 반환합니다.

//users.js

import axios from 'axios';

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

export default Users

이제, 실제로 API를 사용하지 않고 이 메소드를 테스트해보기 위해, 자동으로 가짜의 axios 모듈을 만들기 위해(따라서 느리고 취약한 테스트를 생성) jest.mock 함수를 사용합니다.

모듈을 'mock'한 후에는 테스트에서 주장할 데이터를 반환하는 .get에 대한 mockResolvedValue를 제공할 수 있습니다. 사실상 우리는 axios.get('/users.json')이 가짜 응답을 받길 원한다고 말하고 있습니다.

//users.test.js

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

jest.mock('axios')

test('should fetch users', ()=>{
  const users=[{name: 'Bob'}];
  const resp={data:users};
  axios.get.mockResolvedValue(resp);
  
  //아니면 당신의 케이스에 따라서 다음 것을 사용할 수 있습니다.
  //: axios.get.mockImplementation(()=>Promise.resolve(resp))
  
  return Users.all().then(data=>expect(data).toEqual(users));
});

Mocking Partials

모듈의 일부분은 mock될 수 있고 나머지는 실제 구현을 위해 유지됩니다.

//foo-bar-baz.js
export const foo='foo';
export const bar=()=>'bar';
export default ()=>'baz';
//test.js
import defaultExport, {bar, foo} from "../foo-bar-baz";

jest.mock('../foo-bar-baz',()=>{
  const originalModule=jest.requireActual('../foo-bar-baz');
  
  //defaultEpxort와 foo mocking함
  return{
    _esModule:true,
    ...originalModule,
    default: jest.fn(()=>'mocked baz'),
    foo:'mocked foo',
  };
});

test('should do a partial mock',()=>{
  const defaultExportResult=defaultExport();
  expect(defaultExportResult).toBe('mocked baz')
  expect(defaultExport).toHaveBeenCalled();
  
  expect(foo).toBe('mocked foo');
  expect(bar()).toBe('bar');
});

Mock Implementations

여전히, 리턴값을 구체적으로 명시하고 mock function의 구현으로 완전히 교체하는 것은 능력밖의 일인 경우가 있습니다. 이것은 jest.fn 또는 mock function의 메소드인 mockImplementationOnce를 통해 해결할 수 있습니다.

const myMockFn=jest.fn(cb=>cb(null,true));

myMockFn((err,val)=>console.log(val));
//>true

mockImplementation 메소드는 다른 모듈에서 생성된 mock function의 기본 구현을 정의할 때 유용하게 사용됩니다.

//foo.js
module.exports=function(){
  //구현 내용
}
//test.js
jest.mock('../foo'); // 이것은 automocking에 의해 자동으로 수행됨
const foo=require('../foo');

//foo는 mock function
foo.mockImplementation(()=>42);
foo(); 
//>42

여러 함수 호출이 다른 결과를 생성하도록 하는 복잡한 mock 함수를 만들어내야 할 때, mockImplementationOnce 메소드를 사용하세요.

const myMockFn=jest
	.fn()
	.mockImplementationOnce(cb=>cb(null,true))
	.mockImplementationOnce(cb=>cb(null,false));

myMockFn((err,val)=>console.log(val)); //>true

myMockFn((err,val)=>console.log(val)); //>false

모킹 함수가 mockImplementationOnce에 의해 정의되어 구현이 부족하다면, jest.fn으로 기본 세팅된 구현된 것이 실행될 것입니다.(만약에 기본 세팅된 부분이 정의되어 있다면)

const myMockFn=jest
	.fn(()=>'default')
	.mockImplementationOnce(()=>'first call')
	.mockImplementationOnce(()=>'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
//>'first call', 'second call', 'default', 'default'

일반적으로 묶여있는 메소드들이 있는 경우가 있는데(그래서 언제나 this를 반환해야 하는), 모든 mocks 위에 있는 .mockReturnThis() 함수로 이것을 단순화할 수 있는 sugary API를 가지고 있다.

const myObj={
  myMethod: jest.fn().mockReturnThis(),
}

// myObj와 otherObj는 같은 기능을 한다

const otherObj={
  myMethod: jest.fn(function(){
    return this;
  }),
};

Mock Names

테스트 에러 결과를 보여줄 때 jest.fn() 대신에 다른 모습으로 보여지게 하기 위해서 선택적으로 모킹함수에 이름을 지어줄 수 있습니다. 테스트 결과의 에러를 표시하는 모킹함수를 빠르게 구분해내기 위해서는 이름을 지어주세요.

const myMockFn=jest
	.fn()
	.mockReturnValue('default')
	.mockImplementation(scalar=>42+scalar)
	.mockName('add42');

Custom Matchers

마지막으로, 모킹함수가 어떻게 호출되었는지를 입증하는 것을 덜 어렵게 하기 위해서, 커스텀 matcher 함수를 추가했습니다.

//모킹함수가 적어도 한 번은 호출되었음
expect(mockFunc).toHaveBeenCalled();

//모킹함수가 적어도 한 번은 특정 인자들과 함께 호출되었음
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);

//모킹함수의 마지막 호출이 특정 인자들과 함께 호출되었음
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

//모든 호출과 모킹함수의 이름이 스냅샷으로 남겨짐
expect(mockFunc).toMatchSnapshot();

이 matcher들은 .mock 속성을 검사하는 일반적인 형태의 sugar입니다. 더 자세히 알고 싶거나 맛보기 위해서 당신은 언제나 이것들을 사용해볼 수 있습니다.

//모킹함수는 적어도 한 번 이상 호출되었음
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);

//모킹함수는 적어도 한 번은 특정한 인자들과 함께 호출되었음
expect(mockFunc.mock.calls).toContainEqual([arg1,arg2]);
                                            
//모킹함수의 마지막 호출이 특정 인자들과 함께 호출되었음
expect(mockFunc.mock.calls[mockFunc.mock.calls.length-1]).toEqual([arg1, arg2]);

//모킹함수의 마지막 호출의 첫번째 인자는 '42'임
//(이러한 구체적인 입증에 대한 sugar helper는 없음)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length-1][0]).toBe(42);

//스냅샷이 모킹이 같은 똑같은 횟수로, 동시에, 같은 인자로 이루어졌는지 체크
//이름 또한 체크
expect(mockFunc.mock.calls).toEqual([arg1, arg2])
expect(mockFunc.getMockName()).toBe('a mock name')

matchers들을 모두 살펴보고 싶다면, 참고문헌을 확인해보세요.

profile
웹 프론트엔드 새싹🌱

0개의 댓글