Vue TDD

Vue TDD - Vuex ํŽธ

eddie0329 2020. 7. 30. 16:38
๋ฐ˜์‘ํ˜•

Table of Content

๐Ÿ“Œ00. Introduction

์ด๋ฒˆํŽธ์—์„œ๋Š” vuex๋ฅผ ํ…Œ์ŠคํŒ…ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๊ณต์œ ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ฝ”๋“œ๋Š” vue-test-practice์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.๐ŸŽ‰

ํ•ด๋‹น ํŽธ์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ComponentํŽธ์˜ counter๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ์˜ˆ์ œ์—์„œ api mock์„ ์‹ค์ œ๋กœ ์‹คํ˜„ ํ•ด๋ณด๊ธฐ ์œ„ํ•ด json placeholder์— todos๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์˜ˆ์ œ๋„ ๊ฐ™์ด ์ง„ํ–‰ํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ 01. Testing State

state๋Š” ๋ณดํ†ต getDefaultState ๊ฐ™์€ ํ•จ์ˆ˜ํ˜•์œผ๋กœ ์„ ์–ธ์„ ํ•ด์ฃผ๋Š”๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๊ทธ์ด์œ ๋Š” testing์„ ํ• ๋•Œ ์ข€๋” ํŽธํ•˜๊ฒŒ state์˜ ๊ฐ’์„ ์ดˆ๊ธฐํ™” ์‹œ์ผœ์ค„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

// Store.spec.js
import counterStore, { getDefaultState } from '@/store/modules/counter'; // ์Šคํ† ์–ด๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค.
const { state, getters, mutations, actions } = counterStore; // ๋””์Šค๋Ÿญํ„ฐ๋ง์„ ์‚ฌ์šฉ

// state๋Š” ํ•ญ์ƒ ์ดˆ๊ธฐ ๊ฐ’์€ getDefaultState์˜ ๋ฆฌํ„ด ๊ฐ’์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
describe('State test', () => {
  it('state should be equal getDeFaultState()', () => {
    expect(state).toEqual(getDefaultState());
  });
});
// counter.js

export const getDefaultState = () => ({
  count: 0,
  items: [],
});

export default { namespaced: true, state: getDefaultState(), getters, mutations, actions };

๐Ÿ“Œ 02. Testing getters

getters์—๋Š” ์นด์šดํŠธ๊ฐ€ 0์ด๋ƒ ์•„๋‹ˆ๋ƒ๋Š” ํ•จ์ˆ˜๋ฅผ ์„ ์–ธํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

// Store.spec.js
describe('Getters test', () => {
  describe('isCountZero test', () => {
    it('count 0 should be true', () => {
      const state = { count: 0 };
      // count์˜ ๊ฐ’์ด 0์ด๊ธฐ ๋•Œ๋ฌธ์— true์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      expect(isCountZero(state)).toBeTruthy();
    });
    it('count 1 should be false', () => {
      const state = { count: 1 };
      // count ๊ฐ’์ด 0์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— false์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      expect(isCountZero(state)).toBeFalsy();
    });
  });
});
// counter.js
const getters = {
  isCountZero: state => state.count === 0,
};

๐Ÿ“Œ 03. Testing mutations

๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‘๊ฐ€์ง€ mutations๋ฅผ ์„ ์–ธํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

  • INCREMENT: count์˜ ๊ฐ’์„ 1์ฆ๊ฐ€ ์‹œํ‚ด
  • DECREMENT: count์˜ ๊ฐ’์„ 1๊ฐ์†Œ ์‹œํ‚ด
// Store.spec.js

const { INCREMENT, DECREMENT } = mutations;
describe('Mutation test', () => {
  let state;
  // ๋งค ํ…Œ์ŠคํŠธ ์งํ›„ state๊ฐ’์„ ์ดˆ๊ธฐํ™” ์‹œ์ผœ์คŒ
  beforeEach(() => {
    state = getDefaultState();
  });
  describe('Increment test', () => {
    it('Increment state by 1', () => {
      INCREMENT(state);
      // ํ•œ๋ฒˆ INCREMENT๋ฅผ ํ•ด์ฃผ์—ˆ์œผ๋‹ˆ state.count์˜ ๊ฐ’์€ 1์ด ๋‚˜์™€์•ผํ•ฉ๋‹ˆ๋‹ค.
      expect(state.count).toBe(1);
    });
  });

  describe('Decrement test', () => {
    it('Decrement state by 1', () => {
      DECREMENT(state);
      // ํ•œ๋ฒˆ DECREMENT๋ฅผ ํ•ด์ฃผ์—ˆ์œผ๋‹ˆ state.count์˜ ๊ฐ’์€ -1์ด ๋‚˜์™€์•ผํ•ฉ๋‹ˆ๋‹ค.
      expect(state.count).toBe(-1);
    });
  });
});
// counter.js
const mutations = {
  [INCREMENT](state) {
    state.count += 1;
  },

  [DECREMENT](state) {
    state.count -= 1;
  },
};

๐Ÿ“Œ 04. Testing actions

์ผ๋ฐ˜์ ์œผ๋กœ actions์—๋Š” ๋ณดํ†ต api๊ฐ™์€ ๋น„๋™๊ธฐ ์ž‘์—…์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ json placehoder์—์„œ todos๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ž‘์—…์„ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๋จผ์ € todos๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” api call์€ ์ด๋ ‡๊ฒŒ ์งค์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import * as axios from 'axios';

export const fetchData = () => {
  return axios.get('https://jsonplaceholder.typicode.com/posts/1'); // 1๊ฐœ์˜ ํฌ์ŠคํŠธ๋งŒ ๋ถˆ๋Ÿฌ์˜ค๋Š” api
};

ํ•˜์ง€๋งŒ ์‹ค์ œ ํ…Œ์ŠคํŠธ๋Š” ํ•ด๋‹น api์˜ ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ๋ฐ›๋Š” ๊ฒƒ์ด ์•„๋‹Œ mock์„ ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// Store.spec.js
import { fetchData } from '@/api/todo-service';
import { mockItems } from '../__mock__/item-mock';

// ํ•ด๋‹น ์„œ๋น„์Šค๋ฅผ jest mock ํ•ด์ค๋‹ˆ๋‹ค.
jest.mock('@/api/todo-service');


describe('Async actions test', () => {
  describe('Fetch items test', () => {
    it('get items from mock', async () => {
      // ์ด๋ถ€๋ถ„์—์„œ ๋‚ด๋ ค์˜ฌ ๊ฐ’์„ ์ง€์ •์„ ํ•ด์ค๋‹ˆ๋‹ค.
      fetchData.mockResolvedValue(mockItems);
      // mockItem์€ ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค.

      // export const mockItems = {
      //  data: [1, 2, 3],
      // };

      // sinon.spy()์™€ ์‚ฌ์šฉ๋ฒ•์ด ๊ฐ™์Šต๋‹ˆ๋‹ค.
      // commit์ด ์–ด๋–ป๊ฒŒ ํ˜ธ์ถœ์ด ๋˜์—ˆ๋Š”์ง€ jest์—์„œ ๊ฐ์‹œํ•ด์ค๋‹ˆ๋‹ค.
      const commit = jest.fn();
      await FETCH_ITEMS({ commit });
      expect(commit).toHaveBeenCalledWith('SET_ITEMS', mockItems.data);
    });
  });
});
// Store.spec.js

const actions = {
  async [FETCH_ITEMS]({ commit }) {
    const response = await fetchData();
    commit(SET_ITEMS, response.data);
  },
};

๐Ÿค”๋ฒˆ์™ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ mockํ•˜๊ธฐ

๋Œ€ํ‘œ์ ์œผ๋กœ ์“ฐ์ด๋Š” nanoid๋ฅผ mockํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

import nanoid from 'nanoid';

/**
 * mocking nanoid
 */
jest.mock('nanoid');
let value;
// String์œผ๋กœ 1๋ถ€ํ„ฐ ์ฆ๊ฐ€๋œ๊ฒƒ์„ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค.
nanoid.mockImplementation(() => {
  return String((value += 1));
});
// ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚œํ›„ 0์œผ๋กœ ์ดˆ๊ธฐํ™”๋ฅผ ํ•ด์ค๋‹ˆ๋‹ค.
beforeEach(() => {
  value = 0;
});
// ํ…Œ์ŠคํŠธ๊ฐ€ ๋ชจ๋‘ ๋๋‚œํ›„์— ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์— ์˜ํ–ฅ์„ ๋ฏธ์น ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ nanoid๋ฅผ ๋˜๋Œ๋ ค ๋†“์Šต๋‹ˆ๋‹ค.
afterAll(() => {
  nanoid.mockRestore();
});

๐Ÿ“Œ 05. Conclusion

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ง ๋‹ค๋Š”๊ฒƒ์„ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์ง ๋‹ค๋Š”๊ฒƒ์€ ๊ฐ๊ฐ์˜ ์ฝ”๋“œ๋ฅผ ์งœ๋Š” ๊ฒƒ์˜ ์‹œ๊ฐ„์ด ๋” ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ์ด ์‚ฌ์‹ค์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ •ํ™•ํ•œ ์ฝ”๋“œ๋“ค๋กœ ํ•˜๋‚˜ํ•˜๋‚˜ ์Œ“์•„๊ฐ„๋‹ค๋ฉด ์ „๋ฐ˜์ ์ธ ์‹œ๊ฐ„์„ ํ›จ์”ฌ ๋” ๋‹จ์ถ•์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ„ํ˜น mutations์™€ ๊ฐ™์€ ๊ฐ„๋‹จํ•œ ๊ฒƒ๋“ค์€ ์™œ test code๋ฅผ ์งœ๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ๋‹ค๋Š” ์‚ฌ๋žŒ๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋‘๊ฐ€์ง€ ๊ด€์ ์—์„œ ์ข€๋” ์‚ดํŽด๋ด์•ผํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฒซ ๋ฒˆ์งธ ๊ด€์ ์€ test code๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์€ ๋ฌธ์„œ์˜ ์—ญํ• ์„ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. mutations์— ์ •ํ™•ํ•˜๊ฒŒ ์–ด๋– ํ•œ ๊ฐ’๋“ค์ด ๋“ค์–ด๊ฐ€๋Š”์ง€ ์–ด๋–ค ํƒ€์ž…๋“ค์ด ๋“ค์–ด๊ฐ€๋Š”์ง€ ์ •ํ™•ํ•˜๊ฒŒ ์•Œ์•„์•ผ ํ•  ํ•„์š”์„ฑ์ด ์žˆ์„ ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ ๋Œ๋ ค์„œ ํ™•์ธํ•˜๊ธฐ์—๋Š” ๋‹จ๊ณ„๊ฐ€ ๋ณต์žกํ•œ ๊ฒฝ์šฐ๋„ ์žˆ๊ณ  ํ•œ๋ˆˆ์— ๋ณด๊ธฐ๊ฐ€ ์–ด๋ ค์›Œ ์ง‘๋‹ˆ๋‹ค.

๋‘ ๋ฒˆ์งธ ๊ด€์ ์€ test code๊ฐ€ ์‚ฌ์†Œํ•œ ๋ฒ„๊ทธ๋ฅผ ์žก์•„์ฃผ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ฐ„ํ˜น ์ €๋Š” ์ด๋Ÿฐ ์‹ค์ˆ˜๋ฅผ ํ•˜๊ณค ํ•ฉ๋‹ˆ๋‹ค. state์— tset๋ผ๊ณ  ์„ ์–ธ์ด๋ ‡๊ฒŒ ์„ค์ •์„ ํ•˜๊ณ 
mutations์—๋Š” ๋˜ ์ •ํ™•ํ•˜๊ฒŒ state.test = 1; ์ด๋ ‡๊ฒŒ ํ•ด๋†“๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ ๋ฒ„๊ทธ๋ฅผ ์žก๋Š”๋ฐ ์‹œ๊ฐ„์ด ์ •๋ง ๋งŽ์ด ์†Œ์š”๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฐ ์ž์ž˜ํ•œ ๋ฒ„๊ทธ๋“ค์„ ์žก์•„์ฃผ๊ธฐ ๋•Œ๋ฌธ์— testcode๋ฅผ ์”๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋ชจ๋“  ์ฝ”๋“œ๋“ค์„ 100% tdd๋กœ ๊ฐ€๋Š”๊ฒƒ์€ ๋ถ€์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  100% test code๋ฅผ ์‹ ๋ขฐํ•˜๋Š”๊ฒƒ ๋˜ํ•œ ์œ„ํ—˜ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.(test case์˜ ๋ถ€์žฌ ํ˜น์€ test case๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์„ ๊ฐ€๋Šฅ์„ฑ๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—) ํ•˜์ง€๋งŒ ๋งˆ์น˜ mutations type์„ ์ƒ์ˆ˜ํ™”๋ฅผ ํ•ด๋†“๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ž์ž˜ํ•œ ๋ฒ„๊ทธ๋ถ€ํ„ฐ ์น˜๋ช…์ ์ธ ๋ฒ„๊ทธ๋Š” ๊ฝค๋งŽ์ด ์žก์•„์ค„ ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ•„์š”์— ๋”ฐ๋ผ tdd๋ฅผ ์“ฐ๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ test์˜ ์ข…๋ฅ˜๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์ž…๋‹ˆ๋‹ค. unit test๋กœ๋งŒ ๋ชจ๋“  test๋ฅผ ์ปค๋ฒ„ํ•˜๋ ค๊ณ  ํ•˜์ง€ ๋งˆ์„ธ์šฉ... ๐Ÿ˜…

์ด๋ฒˆ ํฌ์ŠคํŒ… ๊นŒ์ง€๋Š” vue์˜ logic์ ์ธ ๊ฒƒ์„ ์‚ดํŽด ๋ณด์•˜๋‹ค๋ฉด ๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ๋Š” vue์˜ view์ ์ธ ๊ฒƒ์„ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.(Story Book)

๐Ÿ“Œ Reference

๋ฐ˜์‘ํ˜•