프론트(feat Vue, Vuex)로 단위테스트 - 2 : 컴포넌트 TDD

김장훈·2020년 9월 22일
3

Vue단위테스트

목록 보기
2/4

컴포넌트를 TDD로 개발하자

세줄요약

  • 엘리먼트 등 UI적인 이슈는 스냅샷을 통해서 테스트
  • 엘리먼트와 함수의 연결은 toHaveCalled 등을 통해서 테스트
  • 컴포넌트 내의 데이터, 함수 등은 wrapper.vm을 통해서 확인
  • 내가 만들고자 하는 화면은 매우 간단하다.

button.spec.js 생성

  • 차후 구분을 위해 unit 아래 example 폴더를 만들고 button.spec.js 를 생성하였다.
  • component가 제대로 생성 됬는지 확인하는 테스트를 작성하자.
// tests/unit/example/button.spec.js
import { shallowMount } from '@vue/test-utils';
import ButtonPage from '@/components/example/ButtonPage';

describe('ButtonClick', () => {
  it('Page 스냅샷을 찍자', () => {
    const wrapper = shallowMount(ButtonPage);
    expect(wrapper.html()).expect(wrapper.html()).toMatchSnapshot();
  });
});
  • tip) yarn jest --watch를 사용하면 알아서 테스트를 돌려준다. 전체 테스트가 다 실행된 후에 f를 눌러서fail만 실행되게 하는 것을 추천한다.
  • 아무것도 없기에 당연히 fail이 뜬다. 컴포넌트를 만들어서 pass 하게 만들어주자.
  • mount는 연관된 컴포넌트를 진짜 렌더하고, shalloMount는 연관된 컴포넌트를 mock으로 렌더한다.
<template>
  <div></div>
</template>
<script>
export default {
  data() {
    return {};
  },
};
</script>
  • components/example/ButtonPage.vue
  • 다시 테스트를 돌리면 통과를 할 것이다.
  • 스냅샷을 찍어났기에 html 모양이 변경되면 fail이 날 것이고 이때는 u를 눌러줘서 스냅샷을 업데이트 해줘야한다.

기본 모양들 테스트

  • 숫자가 나오는 영역, Click Me가 나오는 영역(버튼) 등이 제대로 있는지 확인해보자.
  • 숫자가 나오는 부분은 component data를 렌더하는 것을 통해서, 버튼은 버튼의 존재 여부 정도로 확인할 수 있다.

숫자 영역 테스트

it('기본 영역을 확인하자 : 숫자 나오는 부분', () => {
    const wrapper = shallowMount(ButtonPage);
    const count = wrapper.find('.count-area').text();
    expect(count).toBe('0');
  });
  • fail이 뜬다. 컴포넌트를 수정하자.
<template>
  <div>
    <div class="count-show">
      <span class="count-area">{{ counter }}</span>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      counter: 0,
    };
  },
};
</script>
  • 스냅샷 테스트가 실패할것인데 u를 눌러서 업데이트 하자. 더 다른 옵션은 w를 누르면 보인다.
  • 여기서 u를 누르면 알아서 업데이트가 된다.
  • 스냅샷까지 업데이트를 하면 모든 테스트가 통과할 것이다.

버튼 테스트

  • 버튼은 크게 버튼 엘리먼트 / 버튼 액션 이렇게 2가지로 구분 지을 수 있다.
  it('기본 영역을 확인하자 : 버튼 렌더 부분', () => {
    const wrapper = shallowMount(ButtonPage);
    const btn = wrapper.find('.increment-btn');
    expect(btn.text()).toBe('Click Me');
  });
  • Fail이 뜨니 이제 컴포넌트를 수정하자.
<template>
  <div>
    <div class="count-show">
      <span class="count-area">{{ counter }}</span>
    </div>
    <div>
      <button class="increment-btn">Click Me</button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      counter: 0,
    };
  },
};
</script>
  • 이제 버튼에 함수를 연결하고 이 함수를 테스트 해보자.
  • 함수를 테스트 할 경우
  1. 함수는 원하는 로직을 수행하는가
  2. 함수가 제대로 호출 되는가
  3. (optional) 함수가 원하는 인자와 함께 호출 되는가
    총 3가지 경우를 확인하려고 한다.

함수가 원하는 로직을 수행하는가

  • component 내의 함수만 테스트를 하는 경우이다.
  • 숫자를 받아서 짝수이면 true, 홀수이면 false를 return 하는 메서드를 만들어보자.
  it('함수의 로직은 정상 작동 하는가', () => {
    const wrapper = shallowMount(ButtonPage);
    let res = wrapper.vm.doSomething(2);
    expect(res).toBe(true);
  });
  • 이런식으로 wrapper의 vm에서 바로 함수를 호출하는 방식을 사용하면 된다.
  • wrapper의 vm은 컴포넌트 내의 data()에 바로 접근할 수도 있다.
<template>
  <div>
    <div class="count-show">
      <span class="count-area">{{ counter }}</span>
    </div>
    <div>
      <button class="increment-btn">
        Click Me
      </button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      counter: 0,
    };
  },
  methods: {
    doSomething(number) {
      return number % 2 == 0 ? true : false;
    },
  },
};
</script>

함수가 제대로 호출 되는가

  • 버튼 클릭하면 버튼에 연결된 함수가 작동해야한다.
  it('버튼의 함수가 제대로 호출 되는가', () => {
    const mockMethod = jest.spyOn(ButtonPage.methods, 'someMethod');
    const wrapper = shallowMount(ButtonPage);
    const btn = wrapper.find('.increment-btn');
    btn.trigger('click');
    expect(mockMethod).toHaveBeenCalled();
  });
  • 확인하고자 하는 메서드 명을 jest.spyOn으로 입력하였다.
  • method가 실행되는지는 toHaveBeenCalled를 통해 확인한다
  • 당연히 실패를 할 것이다. Pass하게 변경하자
<template>
  <div>
    <div class="count-show">
      <span class="count-area">{{ counter }}</span>
    </div>
    <div>
      <button class="increment-btn" @click="someMethod">Click Me</button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      counter: 0,
    };
  },
  methods: {
    someMethod() {
      console.log('someMethod');
    },
  },
};
</script>
  • click에 someMethod를 붙여주었다.

  • 이제 성공할 것이다.

    함수가 인자와 함께 호출 되는가

  • 이 함수가 특정 param을 받거나 data를 전달해야하는 경우도 테스트 해보자.

  • someMethod("name") 등을 받는다고 해보자.

  it('버튼의 함수가 인자와 함께 호출 되는가', () => {
    const mockMethod = jest.spyOn(ButtonPage.methods, 'someMethod');
    const wrapper = shallowMount(ButtonPage);
    const btn = wrapper.find('.increment-btn');
    btn.trigger('click');
    expect(mockMethod).toBeCalledWith('name');
  });
<template>
  <div>
    <div class="count-show">
      <span class="count-area">{{ counter }}</span>
    </div>
    <div>
      <button class="increment-btn" @click="someMethod('name')">
        Click Me
      </button>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      counter: 0,
    };
  },
  methods: {
    someMethod() {
      console.log('someMethod');
    },
  },
};
</script>
  • 인자에는 "name"을 고정 값으로 줬지만 저 부분은 유동적으로 바뀔 부분이고 이는 차후에 테스트에서도 적절하게 반영해줘야한다.

테스트 리팩토링

  • 간단히 테스트를 모두 작성하였는데 이제 리팩토링을 해보자.
  • 중복된 부분을 없애거나 애매한 변수명 등을 제거하는 방향으로 진행할 예정이다.
  • 우선 전체 코드를 봐보자
// tests/unit/example/button.spec.js
import { shallowMount } from '@vue/test-utils';
import ButtonPage from '@/components/example/ButtonPage';

describe('ButtonClick', () => {
  it('Page 스냅샷을 찍자', () => {
    const wrapper = shallowMount(ButtonPage);
    expect(wrapper.html()).toMatchSnapshot();
  });

  it('기본 영역을 확인하자 : 숫자 나오는 부분', () => {
    const wrapper = shallowMount(ButtonPage);
    const count = wrapper.find('.count-area').text();
    expect(count).toBe('0');
  });

  it('기본 영역을 확인하자 : 버튼 렌더 부분', () => {
    const wrapper = shallowMount(ButtonPage);
    const btn = wrapper.find('.increment-btn');
    expect(btn.text()).toBe('Click Me');
  });

  it('함수의 로직은 정상 작동 하는가', () => {
    const wrapper = shallowMount(ButtonPage);
    let res = wrapper.vm.doSomething(2);
    expect(res).toBe(true);
  });

  it('버튼의 함수가 제대로 호출 되는가', () => {
    const mockMethod = jest.spyOn(ButtonPage.methods, 'someMethod');
    const wrapper = shallowMount(ButtonPage);
    const btn = wrapper.find('.increment-btn');
    btn.trigger('click');
    expect(mockMethod).toHaveBeenCalled();
  });

  it('버튼의 함수가 인자와 함께 호출 되는가', () => {
    const mockMethod = jest.spyOn(ButtonPage.methods, 'someMethod');
    const wrapper = shallowMount(ButtonPage);
    const btn = wrapper.find('.increment-btn');
    btn.trigger('click');
    expect(mockMethod).toBeCalledWith('name');
  });
});
  • 내가 가진 개발 능력으론 여기서 리팩토링 할 수 있는 영역은 기껏해야 중복되는 부분을 수정하는 정도이다.
  • 각 테스트 마다 wrapper를 만드는 데 이를 공통으로 빼보자.
  • 테스트 마다(each) 테스트 전에(before) 세팅해줘야 한다. 그래서 beforeEach 를 사용하려고 한다.
// tests/unit/example/button.spec.js
import { shallowMount } from '@vue/test-utils';
import ButtonPage from '@/components/example/ButtonPage';

describe('ButtonClick', () => {
  let wrapper;
  beforeEach(() => {
    wrapper = shallowMount(ButtonPage);
  });
 ...
  • 위 처럼 최상단에 wrapper를 만들어 주고 각 테스트에 있는 wrapper를 모두 삭제해주자
  it('Page 스냅샷을 찍자', () => {
    expect(wrapper.html()).toMatchSnapshot();
  });
  • 이렇게 할 경우 mockMethod를 만든 부분이 에러가 발생한다. 어떤 원인인지 감은 잡히나 명확히 설명은 어렵다... 일단 수정하자.
mport { shallowMount } from '@vue/test-utils';
import ButtonPage from '@/components/example/ButtonPage';

describe('ButtonClick', () => {
  let wrapper;
  let mockSomeMethod;
  beforeEach(() => {
    mockSomeMethod = jest.spyOn(ButtonPage.methods, 'someMethod');
    wrapper = shallowMount(ButtonPage);
  });
  • 위에서 wrapper와 함께 같이 넣어주었다. 그리고 mockMethod의 변수명도 구체적으로 변경하였다.
  it('버튼의 함수가 제대로 호출 되는가', () => {
    const btn = wrapper.find('.increment-btn');
    btn.trigger('click');
    expect(mockSomeMethod).toHaveBeenCalled();
  });
  • mock메서드가 사용되는 부분도 변경
  • 이렇게 하면 일단은 컴포넌트 관련 TDD가 간단하게 완성되었다.
profile
읽기 좋은 code란 무엇인가 고민하는 백엔드 개발자 입니다.

2개의 댓글

comment-user-thumbnail
2020년 9월 23일

잘 봤습니다!!

1개의 답글