Vue TDD

Vue TDD - Storybook ํŽธ

eddie0329 2020. 7. 31. 20:38
๋ฐ˜์‘ํ˜•

Table of Contents

๐Ÿ“Œ 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

๋ฐ˜์‘ํ˜•