뷰 컴포넌트 테스트를 위한 치트시트


원문: https://medium.com/3yourmind/testing-vue-components-a-cheat-sheet-299b3b8be88d

프론트엔드에 대한 단위 테스트를 작성하도록 요청받았을 당시에만 해도 나는 Vue 애플리케이션을 테스트하는 것에 대해 전혀 몰랐다. 그래서 vuejs 테스트에 대해 알아보고 배운 것을 공유하고 싶다. 잘하면 당신은 이 글에서 좋은 것을 골라낼 수 있을 것이다. 이 글과 관련된 모든 예제코드를 보려면 GitHub 저장소를 참고하기 바란다.

이 글에서는 Vue 컴포넌트 단위테스트에 대한 주제만 설명하며, 중간 및 대형 규모의 Vue앱에서 단위 테스트를 작성하는 데 필요한 사항을 요약해 보겠다. 이 글은 테스팅 철학이나 TDD / BDD의 장단점, 또는 소프트웨어 테스트 생명주기에 관한 글이 아니다. 하지만 당신이 Vue 컴포넌트에서 특정한 기능을 테스트 해야 한다면, 계속해서 읽어도 좋다.

설정하기

우리는 단위 테스트를 작성하는데 기본이 되는 두 가지 주요 오픈 소스 도구 vue-test-utilsJest를 사용한다. Jest는 Facebook에서 공개한 테스트 실행 및 확인(assertion) 라이브러리이며 vue-test-utils는 현재 Vue 팀 및 오픈 소스 커뮤니티로 부터 제공되는 공식 테스트 유틸리티 라이브러리이다.

이 글을 읽고 있는 독자는 webpack(또는 유사한)을 사용하여 Vue앱을 만들고 있다고 추측된다. 내가 생각하기에 Vue를 scaffold 하기에 가장 간단한 webpack-simple template로 초기 세팅을 시작하였다. 여기에 vue-test-utilsjest의 추가가 필요하다. 터미널에서 다음을 실행해보자.

yarn add --dev @vue/test-utils jest

우리는 이제 package.json에 test script를 추가할 수 있다.

”test”:./node_modules/.bin/jest”

우선 'test' 라는 폴더를 만들고 거기에 모든 것을 넣는 것으로 충분하다. 나중에, 좀 더 야심 찬 앱을 위한 적절한 폴더 구조를 설명하겠다. 이제 첫 번째 단위 테스트를 작성할 준비가 되었다.

첫번째 테스트

모든 것을 일관되게 유지하기 위해, 단위테스트 명을 테스트하려는 컴포넌트 명과 동일하게 명명하자. Jest는 기본적으로 test.js 또는 spec.js로 끝나는 파일을 찾는다. 우리의 유일한 컴포넌트는 App.vue 임으로 App.test.js 라는 파일을 만들고 신속히 테스트를 수행해보자.

describe('App.vue', () => {
  test('should mount for testing', () => {
    expect(1).toEqual(1);
  });
});

yarn의 테스트 스크립트를 실행하면, '1 equals 1' 이라는 것을 알려주는 사랑스러운 초록 콘솔 메세지가 나타난다. 이 파일에 포함된 내용을 간단하게 알아보자.

describe 함수는 실제로 테스트 중인 컴포넌트를 알려준다. 각 컴포넌트가 하나의 테스트 suite(기본적으로 하나의 컴포넌트나 유틸을 위한 테스트 그룹)를 가지고 있는 구조를 원하기 때문에 각 테스트 파일에는 이들 중 하나만 있어야 한다. describe 함수의 콜백 함수 안에는 assertion(실제테스트)를 그룹화한 test 함수들이 실행되어 테스트를 통과시킨다.

예를 들어 컴포넌트 안에서 다양한 데이터를 그릴 때, 특정 엘리먼트가 어떻게 동작하는지 assertion(실제 테스트들)을 그룹화하는데 test 함수를 사용한다. 각 테스트 파일에는 적어도 하나의 test 블록이 필요하며, 없을 경우에는 실행되지 않는다. test 함수에는 별칭이 존재하며, 일반적인 별칭으로 it을 사용한다.

expect 함수는 특정 상태가 true임을 나타낸다. Jest는 엄격한 동일(toBe), 객체 동등(toEqual)과 같은 것들을 테스트하기 위해 다양한 Matchers를 제공한다. Matchers들은 너무 많은 종류가 있어서 전부 다 설명하기는 힘들며, 더 많은 것을 알고 싶다면 https://jestjs.io/docs/en/expect.html를 확인해보자.

이상으로, Jest가 무엇인지 아주 짧게 소개했다. 현재까지는 vue-test-utils에 관해 전혀 언급하지 않았다. Jest를 사용하여 Vue 컴포넌트를 테스트를 시작해보자.

실제 Vue 컴포넌트 테스트

나는 아주 멋진 카드 컴포넌트를 만들었다.

<template>
  <div
    class="vte__cool-card"
    data-test="cool-card-div"
  >
    hello I am a card :)
  </div>
</template>

<script>
export default {
  name: 'CoolCard'
};
</script>

<style>
.vte__cool-card {
  height: 20vh;
  width: 40vw;
  padding: 2rem;
  border-radius: 1rem;
  box-shadow: 0px 1px 1px #000;
}
</style>

이 컴포넌트에서 주의 깊게 봐야 될 부분은 div의 data-test에트리뷰트 단 하나이다. data-test에트리뷰트는 나중에도 css를 리팩터링 하면서 변경되지 않으므로 테스트시 ID나 class를 대신하여 div에 액세스 할 수 있다.

Vue 컴포넌트를 테스트 하기 전에 설정해야 되는 부분이 한 가지 더 있다. 우선, yarn add--dev babel-jest vue-jest 를 실행한 후, package.json에 다음과 같이 추가한다.

  "jest": {
    "moduleFileExtensions": [
      "js",
      "vue"
    ],
    "transform": {
      "^.+\\.js$": "babel-jest",
      ".*\\.(vue)$": "vue-jest"
    }
  }

아래와 같은 내용의 .babelrc 파일을 어딘가에 배치해야 한다.

  "env": {
    "test": {
      "plugins": ["transform-es2015-modules-commonjs"]
    }
  }

이 설정으로 Jest는 두 가지 일을 할 수 있게 된다. 첫째로, .vue파일을 올바르게 해석한다, 그리고 import문법을 사용할 수 있게 된다 (나머지 코드베이스에서는 아마도 ES모듈을 사용할 것이지만 Jest는 Node에서 실행됨으로 CommonJS 모듈이 사용되므로) 이제 테스트에서도 컴포넌트를 작성하는 문법과 동일한 문법을 사용할 수 있게된다. 더 많은 정보는 jest document 에서 얻을 수 있다.

더 많은 설정 과정이 있었지만, 일단은 넘어가도록 하자. 아래와 같은 테스트에서 특정 컨텐츠의 존재 여부를 테스트 할 수 있다.

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

describe('CoolCard', () => {
  test('should render content correctly', () => {
    const wrapper = mount(CoolCard);
    expect(wrapper.find('[data-test="cool-card-div"]').text()).toEqual(
      'hello I am a card :)'
    );
  });
});

yarn test 를 다시 실행하면, 테스트가 통과하는것을 발견할 수 있다. 이 테스트는 정말 단순한 경우이다(이런 단순한 테스트는 스냅샷 테스트로 커버할 수 있다, 이것에 대해서는 조금 후에 이야기 하겠다). 좀 더 유용하게, 약간의 변경으로 특정 테이블의 행의 개수를 테스트할 수 있다.

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

describe('FakeComponent', () => {
  test('it should display the right number of table rows', () => {
    const wrapper = mount(FakeComponent);
    expect(
      wrapper.findAll('[data-test="fake-table-row"]').wrappers.length
    ).toBe(2);
  });
});

vue-test-utils 문서의 'common tips page'는 거의 모든 Vue 관련 문서와 마찬가지로 매우 훌륭하다. 지금 읽어볼 것을 강력하게 추천한다.


대부분의 사람들이 테스트 환경을 만들 때 테스트에 린터를 통합하는 문제에 마주치게 된다. 이것은 비교적 쉬운 문제이지만, 두 가지 주요한 접근법이 있다. 하나는 .eslintrc 파일이나 유사한 파일을 test 디렉터리에 넣는 것이고, 다른 하나는 기본 eslint 설정 파일에 덮어써서 사용하는 것이다. 필자는 첫 번째 접근법을 더 선호한다, 하지만 설정 파일이 분산되기 때문에 룰을 파악하기가 더 어려워 진다는 단점이 있다. 예제 .eslintrc파일은 다음과 같다.

{
  "globals": {
    "jest": true,
    "expect": true,
    "mockFn": true,
    "config": true,
    "afterEach": true,
    "beforeEach": true,
    "describe": true,
    "it": true,
    "runs": true,
    "waitsFor": true,
    "pit": true,
    "require": true,
    "xdescribe": true,
    "xit": true
  }
}

이 파일은 전역 변수에 대해 허용하는 것으로 표시해야 하는 목적의 린트 설정을 가지고 있다. 만약 단일 폴더 내에 모든 테스트 관련 파일을 놓길 원하지 않는다면(예를 들면 .js 파일별로 테스트 규칙을 유지하려면), 파일 이름을 기반으로 규칙을 작성할 수도 있다. 다음은 당신의 메인 .eslintrc 파일에 추가할 수 있는 간단한 예이다. eslint 문서에서 더 많은 정보를 찾을 수 있다.

 "overrides": [
   {
     "files": [ "**/*.test.js" ],
     "rules": {
       "quotes": [ 2, "single" ]
     }
   }
 ]

많은 사람들이 묻는 "무엇을 테스트해야 합니까"라는 질문은 답하기가 쉽지 않으므로 여기서는 이야기하지 않을 것이다. "무엇을 테스트 해야 하는지"답을 얻기에 유용할 만한 읽을거리를 글의 말미에 열거하였다. 하지만 무엇을 테스트해야 하는지 이미 알고 있다면, 다음에 소개할 패턴들이 당신에게 도움이 될 것이다.

렌더링된 HTML 테스트

스냅샷 테스트는 렌더링된 HTML을 테스트하는 가장 빠른 방법이다. 테스트 비교를 위해 마운트된 컴포넌트를 미리 단순하게 직렬화하여 예기치 않은 UI변경으로부터 보호한다. yarn add jest-serializer-vue 를 실행한 후 pacakge.json의 Jest 섹션에 한 줄을 추가한다.

  "jest": {
    "moduleFileExtensions": [
      "js",
      "vue"
    ],
    "snapshotSerializers": [
      "jest-serializer-vue"
    ],
    "transform": {
      "^.+\\.js$": "babel-jest",
      ".*\\.(vue)$": "vue-jest"
    }
  }

이제 매우 간단하게 스냅샷 테스트를 만들 수 있다.

test('should render content correctly', () => {
  const wrapper = mount(CoolCard, { store, localVue });
  expect(wrapper.vm.$el).toMatchSnapshot();
});

이렇게 하면 나중에 비교할 수 있도록 컴포넌트가 랜더링하는 HTML만 직렬화한다. 만약 실제로 UI가 변경되면 yarn test -u 를 실행하여 스냅샷을 업데이트 할 수 있다. 결과 스냅샷 파일은 항상 버전 컨트롤에 의해 관리되어야 한다.

Method 테스트

Method는 단위 테ì  수 있도록 컴포넌트가 랜더링하는 HTML만 직렬화한다. 만약 실제로 UI가 변경되면 yarn test -u 를 실행하여 스냅샷을 업데이트 할 수 있다. 결과 스냅샷 파일은 항상 버전 컨트롤에 의해 관리되어야 한다.

Method 테스트

Method는 단위 테ì  수 있도록 컴포넌트가 랜더링하는 HTML만 직렬화한다. 만약 실제로 UI가 변경되면 yarn test -u 를 실행하여 스냅샷을 업데이트 할 수 있다. 결과 스냅샷 파일은 항상 버전 컨트롤에 의해 관리되어야 한다.

Method 테스트

Method는 단위 테ì  수 있도록 컴포넌트가 랜더링하는 HTML만 직렬화한다. ë

Computed Properties 테스트

Computed Properties는 암시적 입력(호출된 컨텍스트, 즉 this 또는 더 넒은 스코프 체인에 있는 다른 상태)에 의존하기 때문에 테스트 하기가 더 어렵다. 하지만 Computed Properties는 순수한 함수여야 한다는 것을 생각하여 접근하면, 출력을 테스트 하는것은 쉽다. wrapper.setData 또는 wrapper.setProps 메소드를 사용하여 Vue 컴포넌트에 데이터를 설정할 수 있다. 둘 다 모두 Computed Properties를 테스트 하는데 사용될수 있는 데이터 구성요소를 초기화 할 수 있다. setProps는 이 글을쓰는 시점에서 deprecated 되었으므로 올바른 패턴은 아래와 같다.

  test('someComputedProperty should evaluate buzzwords correctly', () => {
    const wrapper = mount(CoolCard, {
      propsData: {
        wow: 'wow'
      }
    });
    wrapper.setData({
      buzzword: 'Big Data'
    });
    expect(wrapper.vm.someComputedProperty).toEqual('Big Data is very big');
  });

고급 보너스

지금까지 테스트 해본 컴포넌트는 vuex와 같은 store를 사용하지 않고 라우팅을 인식하지 않으며 다른 외부 라이브러리와 같이 사용하지도 않으므로 매우 간단했다. 하지만 좀더 복잡한 컴포넌트나 router view 안에서의 테스트는 mocked(실제를 흉내낸 모방 객체)가 필요할 수 있다.

Mocking the $route object

router또는 더 구체적인 것을 더 쉽게 mock하는 방법은 Vue 인스턴스가 액세스 할 수 있는 route 객체를 사용하는 것이다. 예를들면 Method나 Computed Properties가 $route.path 에 접근하는 테스트의 경우, 우리는 이 프로퍼티를 가진 mock 객체를 생성하고 그 객체를 vue-test-util가 제공하는 mount 함수의 옵션의 인자로 넘겨줄 수 있다.

const $route = {
  params: {
    id: 15,
  },
};

const wrapper = mount(ComplexComponent, {
  mocks: { $route },
});

브라우저 APIs 모방하기

테스트에서 브라우저 API또한 비교적 자주 mocked 되어야 한다 컴포넌트가 localStorage에 의존하는 경우 우리는 localStorage를 mocked 해야 한다. 아래와 같이 테스트 헬퍼 funcion을 사용하여 테스트를 실행하기 위해 stubbed된 기능을 제공함으로써 이 작업을 수행한다.

export function setGlobals() {
  global.window.localStorage = {
    setItem: (key, value) => null,
      getItem: (key) => [],
  };
}

이제 이 helper를 필요로하는 테스트에서 import할 수 있다. 이 경우 테스트에서는 컴포넌트의 localStorage 사용을 실제로 테스트하지 않는다. 그러나 mock이 없으면 컴포넌트가 테스트를 위해 mount 되지 않으므로, 가장 기본적인 속성은 꼭 stubbed되어야 한다.

스토어 모방하기

store 저장소 모방은 Vue 컴포넌트를 테스트하는데 가장 시간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 store에서는 store 모방의 순간이 많이 걸리는 부분이다. 하지만 잘 설계된 currentTotal: () => 789 } } } }); }); test('card should show correct total', () => { const wrapper = mount(CoolCard, { store, localVue }); expect(wrapper.vm.currentTotal).toEqual(789); }); });

이 테스트는 통과한다. 하지만 조금 장황해보인다. component에서 3개 또는 4개의 모듈에 접근한다면, 테스트는 신속하게 관리하기 힘들게 되고 테스트 파일은 커질 것이다. 테스트파일에 직접 store를 moking 하는대신 추상화 하여 유틸리티로 옮겨와 사용한다면 모든 테스트에 적용할 수 있을 것이다. 또한 사용된 beforeEach 함수는, 각 테스트가 독립적이라는 것을 인지하기 전에 store가 리셋되며, 이 부분은 단위테스트의 품질에 중요한 영향을 미친다.



### 스토어 모방을 위한 팩토리 함수

store 구조가 Kevin이 제안한 것과 유사 하다고 가정하고, 아래에 복잡한 컴포넌트를 mount하는데 필요한 mocked된 getter를 전부 해결할 factory 함수의 아주 작은 예제가 아래에 있다.

/* storeFactory.js */

import Vue from 'vue'; import Vuex from 'vuex';

Vue.use(Vuex);

export default (getters, actions) => new Vuex.Store({ modules: { basket: { namespaced: true, actions, getters: { projectId: () => getters.basket.currentTotal || null, currentTotal: () => getters.basket.currentTotal || null } } } });

다음과 같이 테스트 할 수 있다.

/* usingStoreFactory.test.js */

import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import CoolCard from '../components/CoolCard.vue'; import StoreFactory from './utils/StoreFactory';

const localVue = createLocalVue(); localVue.use(Vuex);

const actions = { incrementTotal: jest.fn() };

describe('CoolCard', () => { let store; beforeEach(() => { store = StoreFactory( { basket: { currentTotal: 567 } }, actions ); }); test('card should show correct total', () => { const wrapper = mount(CoolCard, { store, localVue }); expect(wrapper.vm.currentTotal).toEqual(567); }); });

기본적으로 테스트중인 컴포넌트가 의존하는 모든 gatter에 대한 mock값을 정의하면 된다. 이 Factory 함수는 mocked되지 않은 gatter는 null을 반환함으로 완전히 재사용할 수 있다. 필요한 모든 action은 Jest의 jest.fn() mock 함수로 모방한 것이다. 이 패턴의 또다른 이점은 app이 커지고 모듈이 추가되어도 전체 모듈을 모방하는 대신 한두 줄을 추가하는 것으로 대체할 수 있다는 것이다.



단점으로는 store factory 함수를 처음 작성하는데 많은 시간이 소요된다는 점이다. 현재 작업중인 모듈 7개가 있는 중간 크기의 앱의 경우 약 30분이 걸렸다, 하지만 그만한 가치가 있다고 느낀다.


### 보너스 라운드: 테스트 구조

구조에 대해 언급하고자 하는것은 명명 규칙에 관한 것이다. 우리는 모든것을 가능한 한 일관되게 유지하려고 노력해야 한다 예를 들면 SomeComponent.vue라는 컴포넌트가 있으면 해당 테스트의 이름은 항상 SomeComponent.test.js 가 되어야 한다. 이와 같은 사소한 세부 사항들은 신규 개발자들을 최대한 고통 없이 적응하도록 하며, 결국 우리 모두 승리하는 길이다.


### 보너스 라운드 2: Nodelectric Boogavue

아래의 명령어를 치고 크롬 브라우저의 chrome:inspect 를 열면 devtools 이용하여 테스트를 디버깅 할 수 있게 된다. 명령어를 Shell의 별칭으로 설정하고 사용하자.

`node --inspect-brk node_modules/.bin/jest --runInBand`


## 결론

모든 사람들이 직장에서 코드 테스트를 작성하는 것을 좋아하는 것은 아니지만, `vue-test-utils` 덕분에 조금 덜 고통스러울 수 있다. 끝을 맺으며, 충분하지 못한 공간 (또는 지식) 때문에 제대로 전달하지 못한 것들을 보충할 수 있도록 몇 가지 더 많은 책을 추천하고 싶다.


필자는 컴포넌트 중심으로 설명하였으므로 Vuex store의 테스트는 다루지 않았다. [Lachlan Miller 은 Vuex 테스트에 대해 훌륭한 글](https://codeburst.io/a-pattern-for-mocking-and-unit-testing-vuex-actions-8f6672bdb255)을 썼다. 바로 이 글을 읽어보길 추천한다.



무엇을 테스트할지 아는것은 테스트 자체를 작성하는 것에 전문가가 되는 것보다 훨씬 중요하다. 이 주제에 대하여 나는 현재 [Kent C. Dodds의 블로그](https://kentcdodds.com/)를 읽고있다. 그리고 By Evan Burchard가 쓴[Refactoring Javascript](http://shop.oreilly.com/product/0636920053262.do)를 추천한다. 둘 모두 오직 가치있는 것을 테스트하는데 집중하도록 도와주었고, 처음 시작부터 좋은 테스트를 작성하는것이 얼마나 중요한지 다시 알게해줬다. `vue-test-utils`의 배후인 Edd Yerburgh붔천하고 싶다.


필자는 컴포넌트 중심으로 설명하였으므로 Vuhttps://www.manning.com/books/testing-vuejs-applications)을 썼다. 확인해 보기 바란다.

이 블로그의 글은 베를린에서 개발자를 찾고있는 나의 [3YOURMIND](https://www.3yourmind.com/) 고용주의 협력 아래에 작성하였다. 우리는 3D 프린트 분야에서 인정받고 있고 지식이 풍부한 개발자 팀을 가지고 있는 신생 기업이다. 우리는 Vue.js, Django Rest Framework, Spring, Docker등 많은 첨단 기술을 사용하며 당신을 위한 자리가 [여기](https://www.3yourmind.com/career) 있다.

김진우, FE Development Lab2018.08.22Back to list