Table of Contents
- Introduction
- Use of Storybook in vue
- How to bind vuex with storybook
- Conclusion
- Apply to Test
- Reference
๐ Introduction
Vuex, vue component์ ์ด์ storybookํธ ์ ๋๋ค. ์ฌ๋๊น์ง๋ logic์ ๊ดํ ๋ถ๋ถ์ testing ํ๋ค๋ฉด ์ด๋ฒ์๋ view์ ๊ดํ ๋ถ๋ถ์ ํ ์คํ ํ๋ ๋ฐฉ๋ฒ์ ๋ํด ๋งํ๊ณ ์ ํฉ๋๋ค.
View๋ฅผ ํ ์คํ ํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ๊ฐ์ง๊ฐ ์๋๋ฐ ์ Storybook์ด ์ tdd์ ๋ค์ด๊ฐ๋์ง ์์ํ์ค ํ ๋ฐ์. ๊ทธ์ด์ ๋ storybook์ mock๊ฐ์ ๊ทธ๋๋ก ์ฌ์ฉ๊ฐ๋ฅํ๊ณ component๋ฅผ ๋ ๋ฆฝ๋ ํ๊ฒฝ์์ ๊ฐ๋ง ๋ฐ๊พธ์ด๋ณผ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ด๋ฒ ํฌ์คํ
์์๋ ๊ฐ๋จํ๊ฒ checker๋ฅผ ๋ง๋ค์ด๋ณด๋ ์ค์ต์ ํ ์์ ์
๋๋ค. ๐
ํด๋น ํฌ์คํ
์ tdd-todo์ค tdd๋ฅผ ํ๋ฉด์ ๊ณ ๋ฏผ๋์๋ ๋ด์ฉ๋ค์ ๊ธฐ๋ฐ์ผ๋ก ๋ง๋ค์ด์ก์ต๋๋ค.
๐จ ํด๋น ํฌ์คํ ์ storybook์ ์ค์นํ๋ ๋ฐฉ๋ฒ์ ๋ํด์๋ ๋ค๋ฃจ์ง ์์ต๋๋ค. ๐จ
๐ Use of Storybook in vue
์ผ๋จ ๋จผ์ checker๋ฅผ ๋ง๋ค์ด๋ณด๊ฒ ์ต๋๋ค. checker๋ props๋ก isTodoDone์ ๋ฐ๋ผ์ ์ฒดํฌ ๋ชจ์์ธ์ง ์๋๋ฉด ๊ณต๋์ธ์ง ํ์๋ฅผ ํด์ฃผ๋ ์ปดํฌ๋ํธ ์ ๋๋ค.
// Checker.vue
<template>
<div>
// isTodoDone์ด true์ผ๋
<el-button v-if="isTodoDone" type="success" icon="el-icon-check" circle @click="emitClick"></el-button>
// isTodoDone์ด false์ผ๋
<el-button v-else circle icon="el-icon-minus" @click="emitClick"></el-button>
</div>
</template>
<script>
export default {
name: 'CheckButton',
props: {
isTodoDone: {
type: Boolean,
default: false,
},
},
methods: {
emitClick() {
this.$emit('click');
},
},
};
</script>
storybook์ ์ฝ๋๋ ์ด๋ ๊ฒ ์งค์ ์์ต๋๋ค.
// Checker.stories.js
import { storiesOf } from '@storybook/vue';
import CheckButton from '../components/CheckButton.vue';
storiesOf('CheckButton', module)
.add('Not Done', () => ({
data: () => ({
isTodoDone: false,
}),
components: {
CheckButton,
},
methods: {
onClickCheckbtn() {
this.isTodoDone = !this.isTodoDone;
},
},
template: `
<CheckButton :is-todo-done="isTodoDone" @click="onClickCheckbtn"/>
`,
}))
.add('Done', () => ({
components: {
CheckButton,
},
template: `
<CheckButton :is-todo-done="true" />
`,
}));
์ฌ๊ธฐ์ ๋์น๋ฅผ ์ฑ์ ๋ถ๋ ์๊ฒ ์ง๋ง ์ emit์ ์ฌ์ฉํ๋ฉด์ ๊น์ง top-down์ ์์น์ ๊นจ๋์ง ๊ถ๊ธํ์ ๋ถ๋ค์ด ์์๊ฑฐ ๊ฐ์ต๋๋ค. ์ด๋ ๊ฒ ์ค๊ณํ ์ด์ ๋ vuex binding์ ์ค๋ช ํ๋ ๋ถ๋ถ์์ ์์ธํ๊ฒ ์ค๋ช ํ๊ฒ ์ต๋๋ค.
๐ How to bind vuex with storybook
๋ฐฉ๊ธ ๋ง๋ค์๋ checker component์ vuex๋ฅผ ๋ฃ๋๋ค๋ฉด ์ฌ๋ฌ๋ถ๋ค์ ์๋ฌ๋ฅผ ๋ง์ฃผํ๊ฒ ๋ ๊ฒ์ ๋๋ค. ๊ทธ๋์ checker์ ๋ชจ์์ด top down์ ๋ฐฉ์์ด ์๋์๋ ๊ฒ๋๋ค. ๋ฐ๋ผ์ ํญ์ ์ปดํฌ๋ํธ๋ ํจ์ด ์ปดํฌ๋ํธ๋ก ์์ฑ์ด ๋์ด์์ด์ผ ํฉ๋๋ค.
ํจ์ด ์ปดํฌ๋ํธ๋? ์์กด์ฑ์ด ์๋ ์ปดํฌ๋ํธ
๊ทธ๋์ checker์์๋ props๋ก isTodoDone์ ๋ฐ๊ฒ ๋์ด์๊ณ emit์ผ๋ก click ์ด๋ฒคํธ๋ฅผ ํธ์ถํ๊ฒ ๋ฉ๋๋ค. ๊ทธ๋์ ํญ์ ํจ์ด ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ ๋ container๋ฅผ ๋์ด vuex์ฒ๋ฆฌ๋ฑ๊ณผ ๊ฐ์ ์์กด์ฑ์ ๊ด๋ฆฌ ํด์ฃผ๋ ๊ฒ์ ๋ง๋ค๊ฒ ๋ฉ๋๋ค.
// CheckerContainer.vue
<template>
<el-row class="list-todo" data-cy="listTodo">
<el-col :span="2">
<CheckButton :is-todo-done="todo.isTodoDone" @click="changeTodoStatus" data-cy="checkBtn"/>
</el-col>
</el-row>
</template>
<script>
import { mapState, mapAction } from 'vuex';
export default {
name: 'ContainerTodos',
computed: {
...mapState('todos', ['todo']),
},
methods:{
...mapActions('todos', { changeTodoStatus: CHANGE_TODO_STATUS }),
}
};
</script>
์์ ์ฝ๋์ฒ๋ผ vuex์ ๊ฐ๋ค์ container๊ฐ ํ๋ฒ ๊ฐ์ธ ์์กด์ฑ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ ์ ์์ต๋๋ค.
ํน์ ์ด๋ฐ ๋ฐฉ์์ผ๋ก๋ ํด๊ฒฐ์ด ๊ฐ๋ฅํฉ๋๋ค.
// MyCounter2.stories.js
import { storiesOf } from '@storybook/vue';
import { withKnobs } from '@storybook/addon-knobs';
import MyCounter2 from '../components/MyCounter2';
import Vuex from 'vuex';
// const counter = {
// namespaced: true,
// state: {
// count: 0,
// },
// };
storiesOf('MyCounter2', module)
.addDecorator(withKnobs)
.add('Default MyCounter2', () => ({
components: {
MyCounter2,
},
template: `
<div>
<MyCounter2 />
</div>
`,
store: new Vuex.Store({
modules: {
counter: {
namespaced: true,
state: { count: 0 },
},
},
}),
}));
์ด๋ ๊ฒ ํ๋ฉด Vuex์ฒ๋ฆฌ๋ฅผ ํด์ค์ ์์ต๋๋ค. ์ ๋ ์ฐธ๊ณ ๋ก mutations๋ actions๋ ๊ตฌํํ์ง ์์ต๋๋ค. ํด๋น mutations๋ actions๋ ์ด๋ฏธ unit test๋ฅผ ํ์๊ฒ ์ด๊ณ , data(state)์ ๋ํ view testing์ด๊ธฐ ๋๋ฌธ์ state๋ getters์ ๋๋ง ๊ตฌํํด์ค๋๋ค. ํญ์ vue๋ MVVM์ด๊ณ data๋ฅผ ํตํด view๋ฅผ ๊ทธ๋ ค์ฃผ๋ ๊ฒ์ ์ผ๋ ํด์ฃผ์ธ์.
๐ Apply to test
์ด์ ์คํ ๋ฆฌ๋ถ์ผ๋ก ์ด๋ป๊ฒ ํ ์คํธ๋ฅผ ํ๋์ง ์์๋ฅผ ํ๋ ๋ค์ด๋ณด๊ฒ ์ต๋๋ค. ์์๋ Counter๋ก ๋ค์ด๋ณด๊ฒ ์ต๋๋ค.
๋ง์ฝ
Count:0์ ๊ฐ์ 100์ผ๋๋ 10000์ผ๋์ ๊ฐ์ ํ๋ฒ view๋ก ๊ทธ๋ ค๋ณด๊ณ ์ถ๋ค๊ณ ๊ฐ์ ํ๊ฒ ์ต๋๋ค. ๋ํ ์ ํ ์กฐ๊ฑด์ด 100์ดํ ์ผ๋๋ font-size๊ฐ 10px์ด์ด์ผ ํ๊ณ ์ด์์ผ๋๋ 8px์ด์ด์ผ ํ๋ค๊ณ ๊ฐ์ ํด๋ณด๊ฒ ์ต๋๋ค. ์ค์ ๋ก ์ ๋๋ก ๋์์ ํ๋์ง ์ฌ๋ถ๋ฅผ ํ ์คํ ์ ํ๋ ค๋ฉด ์ค์ ๊ฐ์ ์ฌ๋ ค์ฃผ๊ฑฐ๋ ์ฌ๋๋๋ง์ ํด์ค์ผํฉ๋๋ค. ํ์ง๋ง story๋ก data(state)์ด 100์ผ ๊ฒฝ์ฐ 10000์ผ ๊ฒฝ์ฐ๋ฅผ ๋ง๋ค์ด ์ค๋ค๋ฉด ๋น ๋ฅด๊ฒ ํ ์คํ ์ด ๊ฐ๋ฅํฉ๋๋ค.
๐ Conclusion
Storybook์ ์ฌ์ฉ ํ๋ ค๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก CDD๊ฐ ์ด๋ฃจ์ด์ ธ ์์ด์ผ ๊ฐ๋ฅํฉ๋๋ค. CDD๋ฅผ ํ๋ฉด์ ๊ฐ์ฅ ํฌ๊ฒ ์ด๋ ค์ด ๋ถ๋ถ์ ๋๋์ฒด ์ด๋๊น์ง ์ปดํฌ๋ํธ๋ฅผ ๋ณผ ๊ฒ์ธ๊ฐ์ ๋ํ ๋ฌธ์ ์ ๋๋ค. ๋ง์ดํฌ๋ก ๋์์ธ ๊ฐ์ ์ ๋ง ์ธ์ธํ๊ฒ ๋ง์ ๋ถ๋ถ์ component๋ก ๊ฐ์ ธ๊ฐ ์๋ ์๊ณ ์ ์ด๋ ์ปดํฌ๋ํธ๋ depth๊ฐ 2๋จ๊ณ๊น์ง๋ง ๋ค์ด๊ฐ ์ ์๋๋ก ํ๋ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค. ์ด๋ถ๋ถ์ ํ๊ณผ ๋ช ํํ ์์๊ฐ ์ด๋ฃจ์ด์ ธ์ผ ํ๋ ๋ถ๋ถ์ ๋๋ค.
StoryBook์ ์ฌ์ฉํ๋ฉด ๋ ๋ฆฝ๋ ํ๊ฒฝ์์ component๋ฅผ ๋ค๋ฅธ ๊ฐ๋ค๋ก ํ ์คํธ๋ฅผ ํด๋ณผ ์ ์๋ ์ด์ ์ด ์๊ณ ํน์ํ ์ํฉ์์ ๋ฐ์ํ๋ view๋ฅผ ์์ฝ๊ฒ ๋ค์ํ ๋ฐฉ๋ฒ์ผ๋ก test๋ฅผ ํด๋ณผ ์ ์์ต๋๋ค.
๋ค์ํธ์ e2eํ ์คํ ํ๋ ํธ์ผ๋ก ์ฐพ์์ค๊ฒ ์ต๋๋ค.
๐ Reference
'Vue TDD' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Vue TDD - ํ๊ณ ํธ (0) | 2020.08.10 |
---|---|
Test code ์ฝ๊ฒ ์ฐ๊ธฐ - 1 (0) | 2020.08.06 |
Vue TDD - E2E ํธ (0) | 2020.08.02 |
Vue TDD - Vuex ํธ (0) | 2020.07.30 |
Vue TDD - Component ํธ (0) | 2020.07.25 |