테스트 환경에서는 실제 구현한 함수를 그대로 사용할 수 없는 경우가 많습니다.네트워크 비용이 발생하는 외부 API에 의존하는 함수라던가 함수 실행 후 불필요하게 많은 상태(데이터)가 변경되거나 등등 테스트 자체에 집중할 수 없게 방해되는 외부 요인들은 얼마든지 있습니다.
이때 필요한 것이 바로 모의(Mock) 함수입니다.말 그대로 ‘가짜’이기 때문에 네트워크 비용 없이 바로 특정 결과를 반환하게 하거나 의도적으로 에러를 던지거나 함수가 몇 번 호출되었는지 감시하거나 등등 원하는 대로 마음껏 주무를 수 있습니다.따라서 방해 요인을 최소화하고 온전히 테스트에만 집중할 수 있습니다.
개인적으로 테스트에서 처음 ‘Mock(모의)’란 단어를 접했을 때 이해하기 참 어려웠는데 사실은 ‘모의고사’, ‘모의실험’, ‘모의훈련’, ‘목업(Mock-up)’ 등 평소에 많이 접할 수 있는 단어입니다.
[모의]: 실제의 것을 흉내 내어 그대로 해 봄.
간단하게 실제 Todo API를 사용하여 컴포넌트를 구성해 봅시다.다음은 Axios로 Todo 정보를 가져와서 화면에 제목(title
)을 출력하는 예제입니다.
<!-- HelloJest.vue -->
<template>
<div>
{{ todo.title }}
</div>
</template>
<script>
import axios from 'axios'
export default {
data () {
return {
todo: {}
}
},
created () {
this.fetchTodo()
},
methods: {
async fetchTodo () {
//https://jsonplaceholder.typicode.com/const { data } = await axios.get('<https://jsonplaceholder.typicode.com/todos/1>')
this.todo = data
}
}
}
</script>
다음은 실제 응답 결과의 data
값입니다.title
속성만 확인합니다!
{
"completed": false,
"id": 1,
"title": "delectus aut autem",
"userId": 1
}
이제 다음과 같이 테스트를 작성합니다.주의할 점은, 실제 요청/응답을 통해야 하므로 화면에 내용이 반영될 수 있는 대략적인 시간을 고려해야 합니다.따라서 **setTimeout
**을 사용해 1초 후 테스트하도록 작성했습니다.네트워크가 속도가 느리면 2초, 3초 등으로 시간을 늘려야 할 수 있겠네요.
// HelloJest.test.js
import { shallowMount } from '@vue/test-utils'
import HelloJest from '../HelloJest'
describe('HellJest', () => {
let wrapper
beforeEach(() => {
wrapper = shallowMount(HelloJest)
})
test('가져온 텍스트가 정상적으로 렌더링', done => {
setTimeout(() => {
expect(wrapper.text()).toBe('delectus aut autem')
done()
}, 1000) // 1초
})
})
일반적인 네트워크 속도라면 위 테스트는 통과합니다.하지만 실제 요청/응답을 사용하기 때문에 만약 네트워크가 오프라인이면 테스트는 실패하며, 이는 외부 요인에 의한 실패가 됩니다.또한, 네트워크가 온라인이지만 속도가 아주 느리거나 API 서버에 문제가 발생하거나 등등 다양한 외부 요인에 의해 테스트는 실패할 수 있습니다.
그렇다면 이런 외부 요인(네트워크 비용)을 제거하기 위해 **axios.get()
**을 모의 함수로 만들어(Mocking) 필요한 값만 반환하게 만들어 봅시다.실제 요청에서는 title
속성이 포함된 todo
객체를 응답받기 때문에 똑같이 작성합니다.다음과 같이 수정합니다.
// HelloJest.test.js
import { shallowMount } from '@vue/test-utils'
import axios from 'axios'
import HelloJest from '../HelloJest'
describe('HellJest', () => {
let wrapper
beforeEach(() => {
// 반환할 가짜 응답 결과
const response = {
data: {
title: 'delectus aut autem'
}
}
// 컴포넌트가 마운트되기 전에 Mocking합니다.
// axios.get()은 Promise를 반환하기 때문에 mockResolvedValue를 사용합니다.
axios.get = jest.fn().mockResolvedValue(response)
// 위 코드는 다음과 같습니다.
// axios.get = jest.fn(() => new Promise(resolve => resolve(response)))
// 컴포넌트 마운트!
wrapper = shallowMount(HelloJest)
})
test('가져온 텍스트가 정상적으로 렌더링', () => {
// 네트워크에 의존할 필요가 없어서(모의 요청/응답이기 때문에) 더는 setTimeout이 필요하지 않습니다.
expect(wrapper.text()).toBe('delectus aut autem')
})
})
이어서 설명합니다.
위에서 살펴본 것과 같이 **jest.fn()
**은 모의 함수를 반환하는데, 이 반환될 모의 함수의 구현을 함께 작성할 수 있습니다.또한 모의 함수는 기본적으로 호출이 감시되며, 추가로 사용할 수 있는 여러 메소드도 가지고 있습니다.
우선, 위에서 작성했던 예제를 기준으로 함수 구현 작성에 대해서 알아봅시다.실제로 **axios.get
**은 비동기로 데이터를 가져오며, Promise 객체를 반환합니다.우리는 **axios.get
**을 네트워크 비용을 사용하지 않도록 모의 함수로 만들지만, 컴포넌트의 로직이 동작해야 하니 똑같이 Promise가 반환되는 구조를 만들어줘야 합니다.따라서 다음과 같이 **jest.fn()
의 첫 번째 인수로 Promise가 반환될 수 있는 콜백 함수를 만들어 줍니다.jest.fn()
**은 모의 함수를 반환하기 때문에 **.mockImplementation()
**을 사용할 수 있습니다.
모의 함수에 사용할 수 있는 여러 메소드는 아래에서 정리합니다.
axios.get = jest.fn(() => new Promise(resolve => resolve(response)))
// Or
// axios.get = jest.fn().mockImplementation(() => new Promise(resolve => resolve(response)))
특별한 구현이 없고 원하는 값(Promise)만 반환하면 되기 때문에,위 방법을 .mockResolvedValue()
메소드로 대체할 수 있습니다.