VueJS | Vuex Unit Test (1)

Axios를 활용해서 API 호출을 담당하는 Action들의 Unit Test에 대해 다루는 글입니다.


해보고싶다 Unit Test

원칙적으로 TDD를 하고자 하면 먼저 테스트 코드를 작성 한 후에 그에 맞춰서 코드를 작성해 나가는것이라고 알고 있지만 막상 일정을 타이트하게 잡고 개발을 시작하게 되면 TDD라는 것은 까마득하게 잊어 버리게 되는것 같습니다.
그러나 마무리가 되어갈 쯤 TDD 해야 하나 하는 생각이 항상 스쳐지나가고 한번 해봐야지 해봐야지 하다가 매번 미루고 그냥 지나갔었던것 같습니다. 요번에 마침 여유 아닌 여유가 생겨서 TDD를 한번 시도해보았습니다.

이미 개발을 많이 해놓은지라 컴포넌트 단위의 이벤트를 처리하는 것을 테스트 하는 것은 시간이 많이 걸릴것 같고 API 호출을 담당하는 Action들을 조금이라도 Unit Test를 해보면 좋을것 같다는 생각이 들어서 Action Unit Test를 찾아보기 시작했습니다.
실제적인 Vuejs의 Unit Test에 대한 지식은 전무했던지라 하나 하나 검색해보았습니다. 처음에 생각 했을 때는 API 호출하는 기능들을 테스트하려면 DB에 뭔가 더미 데이터가 박혀 있어야 하나 생각 했었는데 이미 TDD 를 하고 계신 백엔드를 담당하시는 팀장님께서 백엔드에서는 호출 하지 않고 Mock을 만들어서 테스트를 한다고 하시더군요…
Mock이라는게 있는지도 몰랐었는데 axios 관련해서도 mock을 검색하니 바로 나오더 군요. axios-mock-adapter라는 것이었습니다. 그런데 Unit Test를 시작하지도 않았던 상태였던지라… 설명을 봐도 어떤 상황에 써야 하는것인지 감이 안오더군요… 이 모듈에 대해서는 제일 마지막에 다루게 될것 같습니다.

아무튼 서두가 많이 길어졌는데 TDD를 하기 위해서는 얕은 지식으로 Test코드를 먼저 구현한 다음에 실제 코딩을 하는것이라고 알고 있었는데 역시 그래야 하는것을 몸으로 부딪히며 느꼈습니다.

지금부터 설명드리려는 과정은 하나의 Action을 테스트 해보기 위해서 기존의 코드를 수정하는 과정들까지 모두 설명드리려고 합니다.

기존 코드 수정하기

1. Action 분리하기

제가 개발한 환경은 vue-cli로 기본 환경을 설정하였고 store폴더에 vuex 관련 설정을 하였었습니다.
아래 코드와 같이 기능에 따라서 module들을 분리해서 구성을 했었고 하나의 module 안에 state, mutations, actions, getters를 모두 담은 구조로 개발해놓은 상태였었습니다.

store/modules/project.js
1
2
3
4
5
6
7
8
9
10
11
const state = {}
const mutations = {}
const actions = {}
const getters = {}
export default {
state,
mutations,
actions,
getters
}

이런 형태로요 그런데…
Action을 Unit Test하는 방법을 찾아보니 Action들을 테스트하기 위해서는 모듈에서 별도의 파일로 분리되어 있어야 테스트 코드에서 Action들만 inject-loader로 삽입하여 활용하기가 용이한것을 알 수 있었습니다. 그래서 아래 구조와 같이 하나의 모듈은 한 폴더에 담고 actions 파일만 따로 분리하였습니다. 파일을 분리하는 방법은 각자가 사용하기 편한 스타일로 하면 될것 같습니다.

1
2
3
4
5
store
└── modules
└── project
├── index.js
└── actions.js
store/modules/project/index.js
1
2
3
4
5
6
7
8
9
10
11
12
import actions from './actions'
const state = {}
const mutations = {}
const getters = {}
export default {
state,
mutations,
actions,
getters
}
store/modules/project/actions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
// 예로 Test 해볼 action 입니다.
setProject ({commit}, pid) {
axios.get(`project/${pid}`)
.then((res) => {
commit('SET_PROJECT', res.data.project)
})
.catch((error) => {
console.log(error)
})
},
// 그외 action 함수들
}

2. API 함수화 하기

여기까지는 뭐 모듈 파일들이 그리 많지 않으니 이렇게 구조 바꿔도 괜찮겠구나 생각했습니다…
또 그런데… 각 Action들에 심어져 있는 axios로 API를 호출하는 부분들을 별도로 함수화 해서 정리해주는 것이 필요했습니다.
이부분에서 느꼈던 것이 아직 개발 경험이 많이 없다 보니 구글에서 검색하면 나오는 자료들을 바탕으로 개발하며 일하다보니 내가 잘하고 있는 건가 하는 의문이 있었는데 그렇지 못하다는 것을 알 수 있었습니다. 그냥 봐왔던 예제들이 거의다 axios.get(), axios.post() 요런 형태들로 직접 호출하는것만 봐왔지 이부분을 분리해서 관리할 수 있겠구나 하는 생각은 해보지 못했었습니다. Action을 Unit Test하는 자료들을 보니 대부분이 API 호출 부분을 분리해서 관리할 수 있구나 하는 것을 알 수 있었습니다.
그래서 Action들 내의 axios 를 활용한 API 호출 관련 코드들을 아래와 같이 별도의 함수로 분리하였습니다.

1
2
3
4
5
6
7
api
└── index.js
store
└── modules
└── project
├── index.js
└── actions.js
api/index.js
1
2
3
4
5
import axios from 'axios'
export const getProject = (pid) => {
return axios.get(`project/${pid}`)
}
store/modules/project/actions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
import * as api from '@/api'
export default {
setProject ({commit}, pid) {
api.getProject(pid) // 이렇게 변경하였습니다.
.then((res) => {
commit('SET_PROJECT', res.data.project)
})
.catch((error) => {
console.log(error)
})
},
}

이제 Unit Test를 해봅시다

위에 설명드린 것과 같이 코드를 구성해놓으면 이제 하나의 Action을 Unit Test 해볼 수 있습니다. api들을 모두 함수화해 놓으면 다른 Action들도 테스트가 가능할것이구요.

관련문서: Vuex Testing

1. Action helper

먼저 문서를 확인해보면 비동기 Action을 테스트하기 위해서는 helper 함수가 필요합니다. helper 함수는 대부분 Action을 사용할 때 어떤 작업을 마치고 난 이후에 Mutation으로 State에 값을 저장하는 일들을 하기 때문에 실행할 Mutation을 인자로 받아서 실제 Action처럼 동작할 수 있도록 해주는 부분 입니다. 저는 아래와 같은 구조로 구성해놓았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
api
└── index.js
store
└── modules
└── project
├── index.js
└── actions.js
test
└── unit
├── helpers
│ ├── index.js
│ └── api.js
└── spcecs
└── actions.spec.js
test/unit/helpers/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 예상되는 변이와 함께 테스팅 액션을 도와주는 헬퍼
export const testAction = (action, payload, state, expectedMutations, done) => {
let count = 0
// 모의 커밋
const commit = (type, payload) => {
const mutation = expectedMutations[count]
try {
expect(mutation.type).to.equal(type)
if (payload) {
expect(mutation.payload).to.deep.equal(payload)
}
} catch (error) {
done(error)
}
count++
if (count >= expectedMutations.length) {
done()
}
}
// 모의 저장소와 전달인자로 액션을 부릅니다.
action({ commit, state }, payload)
// 디스패치된 변이가 없는지 확인
if (expectedMutations.length === 0) {
expect(count).to.equal(0)
done()
}
}

2. Mock 환경 구성하기

그리고 api 실행을 할 때 일정한 요청으로 하더라도 DB에 값이 변한다면 응답 값이 바뀔수도 있기 때문에 api 요청에 대한 일정한 응답을 할 수 있도록 Mock 환경을 구성해주어야 합니다.

관련문서:axios-mock-adapter

여기에서 아래와 같이 axios-mock-adapter를 활용합니다. axios-mock-adapter 자세한 활용 방법은 위에 링크를 확인해보시기 바랍니다.

test/unit/helpers/api.js
1
2
3
4
5
6
7
8
import axios from 'axios'
const MockAdapter = require('axios-mock-adapter')
const mock = new MockAdapter(axios)
mock.onGet('project/1').reply(200, {
data: { project: {id:1, name: 'test1'} }
})

3. 진짜 막판왕 테스트 코드…

여기까지 모든 환경구성이 완료가 되었으면 Action을 Unit Test를 하기 위한 준비가 끝났습니다.

이제 테스트 코드를 작성하면 됩니다.

test/unit/specs/actions.spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { testAction } from '../helpers'
import axios from 'axios'
// Mock Adapter
require('../helpers/api')
// 기존 store 소스 코드를 수정한 이유 Action만 Inject 하기 위해서...
const actionsInjector = require('inject-loader!@/store/modules/project/actions')
// axios를 함수화한 이유 axios를 사용하기 위해서...
const actions = actionsInjector({
'@/api': {
getProject () {
return axios.get(`project/1`)
}
}
})
// 대망의 테스트 코드...
describe('actions', () => {
it('setProject', (done) => {
testAction(actions.default.setProject, null, {}, [
{
type: 'SET_PROJECT',
payload: {
data: { project: {id:1, name: 'test1'} }
}
}
], done)
})
})

테스트 코드에서 위에 설정된 부분들을 구성하기 위해서 앞서 설명드린 환경 구성을 하였습니다.
그리고 테스트 코드를 작성할 때는 헬퍼 함수인 testAction을 활용합니다. 테스트 할 Action, Action에 전달하는 값들, State, 사용될 Mutation과 예상되는 결과 값을 인자로 넣어주면 테스트 코드가 완료 됩니다.
그러면 대망의 테스트 결과를 확인하실수 있을 것입니다.


이 코드는 정말 간단한 코드라서 다른 좀 더 복잡한 코드들을 테스트하는 부분은 저도 앞으로 좀 더 공부해봐야 할것 같습니다. 그리고 Action 내부에서 다른 Action을 dispatch하는 경우를 다음 편에서 다뤄볼까 합니다.

Share Comments