Vue TDD

Vue TDD - Component ํŽธ

eddie0329 2020. 7. 25. 00:54
๋ฐ˜์‘ํ˜•

Table of Content

๐Ÿ“Œ 00. Introduction

๐Ÿšจ์ฃผ์˜: ํ•ด๋‹น๊ธ€์€ ํ™˜๊ฒฝ์„ค์ •์„ ๋‹ค๋ฃจ์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์ธ cli๋กœ jest๋ฅผ ์„ค์ •ํ•œ ํŒŒ์ผ์„ ํ† ๋Œ€๋กœ ์„ค๋ช…์„ ํ•ฉ๋‹ˆ๋‹ค.

์ด ํฌ์ŠคํŒ…์—๋Š” Vue component๋ฅผ ์–ด๋–ป๊ฒŒ testing์„ ํ• ๊นŒ? ์— ๋Œ€ํ•œ ๋ฌผ์Œ์— ๋‹ต๋ณ€์„ ํ•˜๋Š” ๊ธ€์ž…๋‹ˆ๋‹ค. ํ•ด๋‹น ๊ธ€์˜ ์ฝ”๋“œ๋Š” vue-test-practice์— ๊ฐ€์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์ด ํฌ์ŠคํŒ…์—์„œ ๋‹ค๋ฃฐ ์ปดํฌ๋„ŒํŠธ๋Š” ํ•˜๋‚˜์ด๋ฉฐ ๊ฐ„๋‹จํ•œ Counter๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋Š” ์˜ˆ์ œ๋กœ ์„ค๋ช…์„ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  tdd์˜ ๊ฐ€์žฅ ์ค‘์š”ํ•œ tdd cycle์„ ์ค€์ˆ˜ํ•˜์—ฌ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

MyCounter.vue์˜ ๊ธฐ๋Šฅ:

  • count๊ฐ’์€ ํ•ญ์ƒ display๋˜์–ด์•ผ ํ•œ๋‹ค.(์ดˆ๊ธฐ ๊ฐ’ 0)
  • ํ”Œ๋Ÿฌ์Šค ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด count ๊ฐ’์ด ์ฆ๊ฐ€ํ•œ๋‹ค.
  • ๋งˆ์ด๋„ˆ์Šค ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด count ๊ฐ’์ด ๊ฐ์†Œํ•œ๋‹ค.

ํ•ด๋‹น ๊ธฐ๋Šฅ์€ ์‹ค์ œ๋กœ Data์™€ Method๋ฐ–์— ๊ตฌํ˜„์ด ์•ˆ๋จ์œผ๋กœ ์ถ”๊ฐ€์ ์œผ๋กœ created, props, computed๋Š” ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋กœ ์„ค๋ช…ํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ 01. Testing Data

์ฒซ๋ฒˆ์งธ๋กœ data๊ฐ’์—๋Š” count๊ฐ€ 0์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ด๋ ‡๊ฒŒ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

//MyCounter.spec.js
import { mount } from '@vue/test-utils';
import MyCounter from '@/components/MyCounter';

describe('MyCounter unit test', () => {
  // mount component
  const wrapper = mount(MyCounter);
  const { vm } = wrapper;

   // 
   describe('MYCOUNTER DATA TEST', () => {
    describe('data count test', () => {
      it('count should be 0', () => {
        expect(vm.count).toBe(0); // data์˜ count์—๋Š” 0์ด ๋“ค์–ด์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      });
    });
  });
}

๐Ÿš€ ๊นจ์•Œ์ง€์‹

mount์™€ shallow mount์˜ ์ฐจ์ด์ .

mount๋Š” vue ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ props๋‚˜ trigger click์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
shallow mount๋Š” ์ž์‹ ์ปดํฌ๋„ˆ๋Š”ํŠธ๋Š” ๋žœ๋”๋งํ•˜์ง€ ์•Š๋Š”๋‹ค.

// MyCounter.vue
export default {
  name: 'MyCounter',
  data() {
    return {
      count: 0,
    };
  },
};

๐Ÿ“Œ 02. Testing Methods

counter์˜ ๊ฐ’์„ ์กฐ์ •ํ•˜๊ธฐ์œ„ํ•ด ๋‘๊ฐœ์˜ ํ•จ์ˆ˜๋ฅผ ์„ ์–ธํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  • onClickIncrease (counter์˜ ๊ฐ’์„ 1 ์ฆ๊ฐ€์‹œํ‚ค๋Š” ํ•จ์ˆ˜)
  • onClickDecrease (counter์˜ ๊ฐ’์„ 1 ๊ฐ์†Œ์‹œํ‚ค๋Š” ํ•จ์ˆ˜)
describe('MyCounter unit test', () => {
  // testing methods
  describe('MYCOUNTER METHOD TEST', () => {
    // ๋งค ํ…Œ์ŠคํŠธ ์ „์— count์˜ ๊ฐ’์„ 0์œผ๋กœ ์ดˆ๊ธฐํ™” ์‹œํ‚จ๋‹ค.
    beforeEach(() => {
      vm.count = 0;
    });
    // testing increase method
    describe('onClickIncrease test', () => {
      it('increase by 1', () => {
        vm.onClickIncrease(); // count๋ฅผ ์ฆ๊ฐ€ ์‹œ์ผœ๋ณธ๋‹ค.
        expect(vm.count).toBe(1);
      });
    });

    // testing decrease method
    describe('onClickDecrease test', () => {
      it('decrease by 1', () => {
        vm.onClickDecrease(); // count๋ฅผ ๊ฐ์†Œ ์‹œ์ผœ๋ณธ๋‹ค.
        expect(vm.count).toBe(-1);
      });
    });
  });
});

Vue์ฝ”๋“œ๋Š” ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

export default {
  name: 'MyCounter',
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    onClickIncrease() {
      this.count += 1;
    },
    onClickDecrease() {
      this.count -= 1;
    },
  },
};

๐Ÿ“Œ 03. Testing Created

๊ธฐ๋Šฅ

  • created๋ ๋•Œ data.testCreated์— 'TEST_CREATED'๋ฅผ ์„ธํŒ…ํ•ด์ค€๋‹ค.
// testing created
  describe('MYCOUNTER CREATED TEST', () => {
    describe('created test', () => {
      it('testCreated data should be changed blank to TEST_CREATED', () => {
        expect(vm.testCreated).toBe('TEST_CREATED');
      });
    });
  });
export default {
  name: 'MyCounter',
  created() {
    this.testCreated = 'TEST_CREATED';
  },
  data() {
    return {
      testCreated: '',
    };
  },
};

๐Ÿ“Œ 04. Testing Props

๋จผ์ € props๋ฅผ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์œ„์—์„œ ์„ค๋ช…ํ•œ ๊ฒƒ ์ฒ˜๋Ÿผ shallow mount๊ฐ€ ์•„๋‹Œ mount๋กœ ์„ค์ •์„ ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

import { mount } from '@vue/test-utils';
import MyCounter from '@/components/MyCounter';

describe('MyCounter unit test', () => {
  // mount component
  const wrapper = mount(MyCounter, {
    propsData: {
      testProps: 'TEST_PROPS', // ์ด๋ ‡๊ฒŒ ์„ค์ •์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    },
  });
  const { vm } = wrapper;

  // testing props
  describe('MYCOUNTER PROPS TEST', () => {
    describe('testProps test', () => {
      it('testProps should be "TEST_PROPS', () => {
        expect(vm.testProps).toBe('TEST_PROPS');
      });
    });
  });
});
export default {
  name: 'MyCounter',
  props: {
    testProps: {
      type: String,
    },
  },
};

๐Ÿ“Œ 05. Testing Computed

๊ธฐ๋Šฅ:

data.tempText('HELLO')๋ฅผ ์†Œ๋ฌธ์ž๋กœ ์ถœ๋ ฅํ•˜๋Š” computed

describe('MYCOUNTER DATA TEST', () => {
  // testing computed
  describe('MYCOUNTER COMPUTED TEST', () => {
    it('lowercaseTempText test', () => {
      expect(vm.lowercaseTempText).toBe('hello');
    });
  });
});
export default {
  name: 'MyCounter',
  data() {
    return {
      tempText: 'HELLO',
    };
  },
  computed: {
    lowercaseTempText() {
      return this.tempText.toLowerCase();
    },
  },
};

๐Ÿ“Œ Testing vuex in component

component์—์„œ vuex์˜ test ์‹œ์ž‘์€ mock์—์„œ ๋ถ€ํ„ฐ ์ถœ๋ฐœ ํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๋กœ์ง์ด ์ˆ˜ํ–‰์ด ๋˜๋Š”๊ฑด ๋”ฐ๋กœ vuex์—์„œ ํ…Œ์ŠคํŠธ ํ•ด์ฃผ์„ธ์š”.

// ๋จผ์ € vuex๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import MyCounter2 from '../../src/components/MyCounter2.vue';

// ์ด๋ ‡๊ฒŒ localVue์—์„œ vuex๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
const localVue = createLocalVue();
localVue.use(Vuex);

describe('MyCounter2 test', () => {
  let store;
  let state;
  let actions;
  let mutations;
  let wrapper;
  let vm;
  beforeEach(() => {
    state = {
      count: 0,
    };
    mutations = {
      ['INCREMENT']: jest.fn(),
      ['DECREMENT']: jest.fn(),
    };
    actions = {
      ['FETCH_ITEMS']: jest.fn(),
    };
    store = new Vuex.Store({
      modules: {
        counter: {
          namespaced: true,
          state,
          mutations,
          actions,
        },
      },
    });
    wrapper = mount(MyCounter2, { store, localVue });
    vm = wrapper.vm;
  });
  it('count mock', async () => {
    expect(vm.count).toBe(0);
  });
  it('mutations mock', () => {
    const incBtn = wrapper.find('#incBtn');
    incBtn.trigger('click');
    expect(mutations.INCREMENT).toHaveBeenCalled();

    const decBtn = wrapper.find('#decBtn');
    decBtn.trigger('click');
    expect(mutations.DECREMENT).toHaveBeenCalled();
  });
  it('actions mock', () => {
    const fetchBtn = wrapper.find('.fetchBtn');
    fetchBtn.trigger('click');
    expect(actions.FETCH_ITEMS).toHaveBeenCalled();
  });
});

๐Ÿ“Œ Conclusion

๋ทฐ์˜ ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ๋Š” ์‚ฌ์‹ค ๊ทธ๋ ‡๊ฒŒ ์–ด๋ ต์ง€ ์•Š์Šต๋‹ˆ๋‹ค.. ํ•˜์ง€๋งŒ ์‹ค๋ฌด์—์„œ ์ ์šฉํ•˜๊ธฐ๋Š” ์ƒ๋‹นํžˆ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ๊ทธ์ด์œ ๋Š” ๊ฐ์ข… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค๊ณผ utils๋“ค ๋•Œ๋ฌธ์— ์˜์กด์„ฑ ๋ฌธ์ œ ๋•Œ๋ฌธ์— test๊ฐ€ ์–ด๋ ค์›Œ์ง‘๋‹ˆ๋‹ค. ์ด๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ตœ๋Œ€ํ•œ logic์„ store๋กœ ๋นผ์„œ logic์ ์ธ unit test๋Š” store์—์„œ ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ View์™€ Store๋ฅผ ๋ถ„๋ฆฌ๋ฅผ ํ•˜์—ฌ View์—์„œ๋Š” snapshot ํ…Œ์ŠคํŠธ, storybook, ํ˜น์€ e2eํ…Œ์ŠคํŠธ๋กœ ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๊ณ  store๋Š” unit์œผ๋กœ ํ…Œ์ŠคํŒ…์„ ํ•˜๋Š”๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ๋Š” store๋ฅผ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Reference

๋ฐ˜์‘ํ˜•