1. 목 함수 (Mock Function)
목 함수란 실제가 아닌 테스트를 위해 만든 모형 함수이다. 특정 기능에 집중하는 목 함수를 적절히 활용하여 효율적인 테스트 코드를 작성할 수 있다.
예를 들어 유저 DB에서 user list를 select 하는 작업 구현 시 작성해야 할 코드가 많아지며(테스트 코드보다 많아질 수도) 네트워크, DB 상태 등 외부 영향을 받기도 한다.
이를 목 함수로 테스트 한다면 같은 코드는 동일한 결과를 내는 것이 중요하다.
2. 사용 방법
2-1. 목 함수 생성
목 함수를 생성하고 프로퍼티를 알아본다.
const mockFn = jest.fn() // 목 함수 생성
mockFn() // 목 함수 호출
mockFn(1) // 인수 넘기기
test('함수는 두 번 호출 된다.', () => {
console.log(mockFn.mock.calls)
expect(mockFn.mock.calls.length).toBe(2) // calls.length === 호출된 횟수
})
test('두 번쨰로 호출된 함수에 전달된 첫 번째 인수는 1이다.', () => {
expect(mockFn.mock.calls[1][0]).toBe(1)
})
-
생성된 목 함수에는 mock 프로퍼티가 있고 calls라는 배열을 갖고있다.
-
mock
: 호출되었던 프로퍼티들이 저장 -
calls
: 함수가 몇번 호출됐는 지, 호출될 때 전달된 인수는 무엇인 지 저장 -> 전달된 인수의 값이 배열로 들어있음
-
2-2. 목 함수로 간단한 테스트
빠르고 간단한 테스트를 위해 목 함수를 활용한다.
인수로 받은 배열을 반복하면서 1씩 증가한 값을 콜백으로 넘겨주는 forEachAdd
함수를 테스트한다.
콜백 함수를 새로 만들어서 전달하지 않아도 목 함수를 이용해서 forEachAdd1
함수가 잘 동작하는 것을 확인할 수 있다.
const mockFn = jest.fn()
const forEachAdd1 = (arr: number[]) => {
arr.forEach(num => mockFn(num + 1))
}
forEachAdd1([10, 20, 30])
test('함수는 3번 호출 된다.', () => {
expect(mockFn.mock.calls.length).toBe(3)
})
test('전달된 값은 11, 21, 31이다.', () => {
expect(mockFn.mock.calls[0][0]).toBe(11)
expect(mockFn.mock.calls[1][0]).toBe(21)
expect(mockFn.mock.calls[2][0]).toBe(31)
})
2-3. 값을 리턴하는 목 함수
값을 리턴하는 목 함수를 작성한다.
const mockFn = jest.fn(num => num + 1)
mockFn(10)
mockFn(20)
mockFn(30)
test('함수는 3번 호출 된다.', () => {
console.log(mockFn.mock.results) // [{ type: 'return', value: 리턴값 }]
expect(mockFn.mock.calls.length).toBe(3)
})
test('10에서 1 증가한 값이 반환된다.', () => {
expect(mockFn.mock.results[0].value).toBe(11)
})
test('20에서 1 증가한 값이 반환된다.', () => {
expect(mockFn.mock.results[1].value).toBe(21)
})
-
results : 목 함수에서 리턴된 값이 배열로 들어있다.
-
실행할 때마다 다른 값을 리턴하려면
mockFn.mockReturnValue
을 사용한다.- 중간에 값을 바꾸려면
mockFn.mockReturnValue
사용
- 중간에 값을 바꾸려면
2-4. 비동기 목 함수
mockResolvedValue를 사용하여 비동기 목 함수를 작성할 수 있다.
const mockFn = jest.fn()
mockFn.mockResolvedValue({ name: 'Mike' })
test('받아온 이름은 Mike', () => {
mockFn().then(res => {
expect(res.name).toBe('Mike')
})
})
2-5. 외부 코드 활용 테스트
DB에 유저를 생성하는 외부 코드를 테스트한다고 생각해보자. 테스트마다 실제 유저되면 곤란하고 다시 삭제하는 것도 번거롭다.
이럴 때 jest.mock()
으로 모듈을 모킹한다.
fn.ts
const fn = {
// 실제로 유저 DB에 유저를 생성해주는 함수라고 가정
createUser: (name: string) => {
console.log('실제로 사용자가 생성되었습니다.')
return {
name,
}
},
}
fn.test.ts
import fn from './fn'
//////////////////////////// 모듈 모킹 코드 시작
jest.mock('./fn') // fn을 모킹 모듈로 만듦
// fn.createUser를 사용하고, {name: 'Mike'} 객체를 리턴하도록 설정
(fn.createUser as jest.MockedFunction<typeof fn.createUser>).mockReturnValue({
name: 'Mike',
})
//////////////////////////// 모듈 모킹 코드 끝
// 테스트 성공
test('유저를 생성한다', () => {
const user = fn.createUser('Mike')
expect(user.name).toBe('Mike')
})
- 모듈 모킹 코드로 인해 실제
fn.createUser
는 호출되지 않고(로그가 찍히지 않음),mockReturnValue
를 객체를 리턴하는 목 함수가 동작한다.
실행하면 테스트는 성공하고 로그는 보이지 않는다. === 실제 유저가 생성되지 않음 === 실제 함수가 동작하지 않음
모듈 모킹 코드 주석을 해제하면 fn.createUser
의 로그가 찍힌다. === 실제 함수가 동작함
3. 유용한 매처
아래 toBeCalled, toBeCalledTimes, toBeCalledWith, lastCalledWith 매처를 활용한 테스트는 모두 성공한다.
const mockFn = jest.fn();
mockFn(10, 20);
mockFn();
mockFn(30, 40);
test("한번 이상 호출 되었나?", () => {
expect(mockFn).toBeCalled(); // 한번이라도 호출 됐는 지
});
test("정확히 세번 호출 되었나?", () => {
expect(mockFn).toBeCalledTimes(3); // 정확한 호출 횟수
});
test("10이랑 20 전달받은 함수가 있는가?", () => {
expect(mockFn).toBeCalledWith(10, 20); // 인수로 어떤 값들을 받았는 지 체크 --> 30, 40으로 변경해도 성공
});
test("마지막 함수는 30이랑 40을 받았는가?", () => {
expect(mockFn).lastCalledWith(30, 40); // 마지막으로 실행되는 함수의 인수만 체크 --> 10, 20으로 변경하면 실패
}); // 10, 20으로 바꾸면 실패