O slideshow foi denunciado.
Utilizamos seu perfil e dados de atividades no LinkedIn para personalizar e exibir anúncios mais relevantes. Altere suas preferências de anúncios quando desejar.
lumi.kim(김아름)

2019.11.13

@kakao_fe_meetup
바닥부터시작하는
Vue테스트와리팩토링
🚨주의🚨
테스트방법은다양하고,
오늘제시하는생각들이정답이아닐수있으나
더좋은테스트와테스트하기좋은코드를탐구하는과정공유
오늘말하고싶은것
1. Vue컴포넌트테스트작성방법

2. 테스트를작성하면서했던생각들
3.FE파트안에서관련주제로나눈대화
발표대상
-Vue에대한기본문법을알고있는개발자
- 간단한유닛테스트작성경험이있는개발자
- Vue컴포넌트테스트는어떻게시작하면좋을지궁금한개발자
-그냥듣고싶은개발자
KakaoforBusiness
카카오비즈니스사용자를위한통합비즈니스플랫폼
Kakao for Business
카카오 비즈니스 사용자를 위한
통합 비즈니스 플랫폼
KakaoforBusiness
카카오비즈니스사용자를위한통합비즈니스플랫폼
Nuxt기반의 Vue컴포넌트로
개발된 프로젝트



어느 날부터 혼자 FE운영 시작!
그러던 어느 날….


기존에구현되어있었던덩치가큰몇몇코드들.





기존에구현되어있었던덩치가큰몇몇코드들.



유지보수가계속들어오면서,덩치를키움.

기존구조를유지하며기능추가를하다보니점점수정하는비용이커짐.



기존에구현되어있었던덩치가큰몇몇코드들.



유지보수가계속들어오면서,덩치를키움.

기존구조를유지하며기능추가를하다보니점점수정하는비용이커짐.

나:

더수정되기전에리팩토링으로(=제눈에익숙한코드로)바꾸고싶어요!





기존에구현되어있었던덩치가큰몇몇코드들.



유지보수가계속들어오면서,덩치를키움.

기존구조를유지하며기능추가를하다보니점점수정하는비용이커짐.

나:

더수정되기전에리팩토링으로(=제눈에익숙한코드로)바꾸고싶어요!



P:
...


기존에구현되어있었던덩치가큰몇몇코드들.



유지보수가계속들어오면서,덩치를키움.

기존구조를유지하며기능추가를하다보니점점수정하는비용이커짐.

나:

더수정되기전에리팩토링으로(=제눈에익숙한코드로)바꾸고싶어요!



P:
...
운영 중인 (귀한) 코드를 리팩토링 할 때
테스트코드가 안정성을 도와주고,
추상적인 진행상황을 가시적으로 측정가능하게 해줌!
테스트를 시작한 이유 1
운영업무 외의 도전과제.

(프로젝트 코드를 대상, 컴포넌트 테스트 학습과 적용)


테스트가 없던 프로젝트에 환경을 마련하고 

추후 추진력있게 테스트를 작성할 수 있는 기반 마련.
테스트를 시작한 이유 2
환경구성
컴포넌트
생성 테스트
컴포넌트
데이터 테스트
컴포넌트 간
테스트
환경구성
│ …
├─pages (component)
│ │ …
│ ├─inspection
│ │ inspectionDetail.vue
│ │ …
├─tests
│ ├─mock
│ │ approvedDetails.js
│ │ re...
│ …
├─pages (component)
│ │ …
│ ├─inspection
│ │ inspectionDetail.vue
│ │ …
├─tests
│ ├─mock
│ │ approvedDetails.js
│ │ re...
│ …
├─pages (component)
│ │ …
│ ├─inspection
│ │ inspectionDetail.vue
│ │ …
├─tests
│ ├─mock
│ │ approvedDetails.js
│ │ re...
테스트작성파일셋팅
test('앱정보가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {
  // given
  const option = {
    mocks: {
      $route: {
       ...
import test from 'ava'
import sinon from 'sinon'
import { mount, shallowMount } from '@vue/test-utils'
import InspectionPa...
import test from 'ava'
import sinon from 'sinon'
import { mount, shallowMount } from '@vue/test-utils'
import InspectionPa...
컴포넌트 생성
테스트
서비스검수신청진입페이지.
시나리오
페이지 진입시 (페이지 컴포넌트 생성)
아이디정보가 없으면,
에러 모달을 보여준다.
첫번째테스트작성…
test('앱정보가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {
  // given
  const option = {
    mocks: {
      $route: {
       ...
test('아이디정보가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {
  // given
  const option = {
    mocks: {
      $route: {
        query: ...
function createOption (query) {
  const FAKE_ID = 123456
  const defaultQuery = { id: FAKE_ID }
  
  return {
    mocks: {...
import InspectionPage from '~/pages/.../inspectionDetail.vue'
test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {
  ...
import InspectionPage from '~/pages/.../inspectionDetail.vue'
test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {
  ...
import InspectionPage from '~/pages/.../inspectionDetail.vue'
test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {
  ...
import InspectionPage from '~/pages/.../inspectionDetail.vue'
test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {
  ...
🚨에러해결🚨=>테스트더블복원유틸추가
function restore (...testDoubles) {
  testDoubles.forEach(obj => obj.restore && obj.restore())
}
test(...
테스트더블을붙이는시기에따라,restore()필요여부가달랐음.
mounted이전의LifeCycleHook으로호출되는로직체크시
전역컴포넌트객체에테스트더블을붙이므로
전역컴포넌트가오염되지않도록
restore()사용이필요했지만,...
테스트더블을붙이는시기에따라,restore()필요여부가달랐음.
mounted이전의LifeCycleHook으로호출되는로직체크시
전역컴포넌트객체에테스트더블을붙이므로
전역컴포넌트가오염되지않도록
restore()사용이필요했지만,...
🙄리팩토링+테스트전략 🙄
LifeCycleHook안의코드를
쉽게테스트하려면?
• 추상화된메서드들을호출하는코드만있는게좋지않을까?
• 👉호출시점이프레임워크에서제어되고,
• 👉테스트더블을붙여테스트하기수월
• 컴포넌트곳곳에 h...
컴포넌트 데이터
테스트
{
• props
• data
• computed


데이터를변경시키는…
• (watch)
• (methods)
테스트 해야 할
데이터
종류
{
• <template>표현식결과값
• props
• data
• computed
데이터를변경시키는…
• (watch)
• (methods)
테스트 해야 할
데이터
종류
1)템플릿에표현식이있을때
<!-- 1: 템플릿에 로직이 있음 -->
<div
  ref="headline"
  v-if=“types.includes(INSPECTION_TYPES.FOO)">
  <h4 class="ti...
<!-- 1: 템플릿에 로직이 있음 -->
<div
  ref="headline"
  v-if=“types.includes(INSPECTION_TYPES.FOO)">
  <h4 class="title">타이틀</h4>
...
2)템플릿표현식을제거하면,
<!-- 2: 템플릿에 데이터만 바인딩 -->
<div v-if="hasFooType">
  <h4 class="title">타이틀</h4>
  <p class="desc">디스크립션</p>
...
2)템플릿표현식을제거하면,치환된computed결과값을테스트
<!-- 2: 템플릿에 데이터만 바인딩 -->
<div v-if="hasFooType">
  <h4 class="title">타이틀</h4>
  <p class...
수다 중에 PO 관련 내용이 나와서 공유를 드려볼께요.



PO (Page Object) 관련 내용입니다.
https://martinfowler.com/bliki/PageObject.html🐨
근래에 자주 이야기하는 mvvm에서 view template 이 로직을 나누어
가지지 말자는 이야기도 이것과 통하는 이야기라서 덧붙이자면.
테스트 코드가 view를 그리는 레벨과
최대한 직접 관계를 맺지 않는 장치에요....
그래서. 템플릿에 :if="isRight && isNotLeft, ..." 처럼
view 를 관여하는 로직이 포함되어 있다면 어서 제거를...🐨
(찔림)
자세히는 처음 알았어여
감사합니당
그래서. 템플릿에 :if="isRight && isNotLeft, ..." 처럼
view 를 관여하는 로직이 포함되어 있다면 어서 제거를...
바쁘시겠지만 아지트나 위키에 기록...
🙄리팩토링+테스트전략 🙄
• 리팩토링
• 👉템플릿에서로직(표현식)을분리(computed,methods활용)
• 테스트전략
• 👉DOM에접근해(ex.ref,class,id)렌더링된결과물을확인하기보다는,
• 👉컴포넌트데이터...
간략하게..snapshot테스팅동작방식
[출처] https://medium.com/@luisvieira_gmr/snapshot-testing-react-components-with-jest-3455d73932a4
간략하게..snapshot테스팅동작방식
test('선택한 검수타입에 [Foo]가 있으면, Foo 헤드라인을 보여준다.', t => {
  // given
  const wrapper = mount(InspectionPa...
# Snapshot report for `tests/…/inspectionDetaill.spec.js`
The actual snapshot is saved in `inspectionDetail.spec.js.snap`....
저장된snapshot
│ │ …
├─tests
│ ├─snapshot
│ │ approvedDetail.spec.js.md
│ │ approvedDetail.spec.js.snap
│ │ …
│ ├─specs
│ │ i...
{
• <template>표현식결과값
• props
• data
• computed
데이터를변경시키는…
• (watch)
• (method)
const TITLE = '테스트 타이틀'
const DESCRIPTION =...
{
• <template>표현식결과값
• props
• data
• computed
데이터를변경시키는…
• (watch)
• (method)
뷰인스턴스생성시,주입
뷰인스턴스생성후,조작
const TITLE = '테스트 ...
{
• <template>표현식결과값
• props
• data
• computed
데이터를변경시키는…
• (watch)
• (methods)
뷰인스턴스생성시,주입
뷰인스턴스생성후,조작
- 데이터변화확인
- 메서드호출,...
(나는망각의동물…🙈)
컴포넌트 간
테스트
mountvsshallowMount
[출처] https://vuejsdevelopers.com/2019/09/30/stubs-vue-unit-test/
mountvsshallowMount(렌더링결과비교)
<!-- 1. mount의 렌더링 결과값 -->
<div>
  <div class="bg"></div>
  <div class="layer">
    <div clas...
특정자식컴포넌트만선택해서테스트하려면?
// 부모가 자식에게
test(‘페이지에서 메시지를 전달하며 에러모달을 열면, 모달컴포넌트에 prop으로 전달된다.', t => {
  // given
  const MODAL_MS...
부모가자식에게props를전달할때
// 부모가 자식에게
test(‘페이지에서 메시지를 전달하며 에러모달을 열면, 모달컴포넌트에 prop으로 전달된다.', t => {
  // given
  const MODAL_MSG =...
자식이부모에게emit이벤트를전달할때
// 자식이 부모에게
test(‘페이지에서 열린 에러모달에서, show이벤트가 오면 값이 페이지에 업데이트 된다.', t => {
  // given
  const MODAL_MSG ...
과정을통해
얻은것과아쉬운점
기술적으로… 💻
1. 테스트역할의이해:리팩토링안정성,가시성
2. 컴포넌트테스트방법과노하우
3. 테스트가쉬운컴포넌트고민->근거있는리팩토링->코드일관성
얻은 것 1
기술적으로… 💻
1. 테스트역할의이해:리팩토링안정성,가시성
2. 컴포넌트테스트방법과노하우
3. 테스트가쉬운컴포넌트고민->근거있는리팩토링->코드일관성
얻은 것 1
기술적으로… 💻
1. 테스트역할의이해:리팩토링안정성,가시성
2. 컴포넌트테스트방법과노하우
3. 테스트가쉬운컴포넌트고민->근거있는리팩토링->코드일관성
얻은 것 1
개인적으로… .
1. 업무가소강(?)상태일때할수있는일
2. 운영외의업무로성장하는느낌
3. 피드백에대한갈증해소
얻은 것 2
개인적으로… .
1. 업무가소강(?)상태일때할수있는일
2. 운영외의업무로성장하는느낌
3. 피드백에대한갈증해소
얻은 것 2
개인적으로… .
1. 업무가소강(?)상태일때할수있는일
2. 운영외의업무로성장하는느낌
3. 피드백에대한갈증해소
얻은 것 2
아쉬웠던 점 (feat. 아주 현실적인) 🙈
- 진행상황에대한업무가시화(목록,일정)를잘하지못함
(무엇을해야하고,어디까지해야하고,얼마나걸릴지감없음)
(감이없어서감으로가시화)
그럼에도 불구하고
다음스텝, 남은고민.. 👽
- 기존코드를망가뜨리지않으면서테스트와리팩토링을잘병행하는방법
- 컴포넌트와협력하는외부객체에대한테스트
- 테스트코드의유지보수경험
- 더복잡한케이스의테스트…
다음스텝, 남은고민.. 👽
- 기존코드를망가뜨리지않으면서테스트와리팩토링을잘병행하는방법
- 컴포넌트와협력하는외부객체에대한테스트
- 테스트코드의유지보수경험
- 더복잡한케이스의테스트…
다음스텝, 남은고민.. 👽
- 기존코드를망가뜨리지않으면서테스트와리팩토링을잘병행하는방법
- 컴포넌트와협력하는외부객체에대한테스트
- 테스트코드의유지보수경험
- 더복잡한케이스의테스트…
감사합니다 😺
lumi.kim@kakaocorp.com
Reference
- vue-test-util 공식가이드: https://vue-test-utils.vuejs.org
- vue-test-util 개발자 블로그: https://eddyerburgh.me
- https:...
Q&A
Próximos SlideShares
Carregando em…5
×

바닥부터 시작하는 Vue 테스트와 리팩토링

lumi.kim(김아름) / 카카오
Vue 프로젝트의 코드를 리팩토링하기 위해 테스트 코드를 작성했던 과정과 그 경험을 통해 얻은 인사이트를 공유합니다.

  • Seja o primeiro a comentar

바닥부터 시작하는 Vue 테스트와 리팩토링

  1. 1. lumi.kim(김아름)
 2019.11.13
 @kakao_fe_meetup 바닥부터시작하는 Vue테스트와리팩토링
  2. 2. 🚨주의🚨 테스트방법은다양하고, 오늘제시하는생각들이정답이아닐수있으나 더좋은테스트와테스트하기좋은코드를탐구하는과정공유
  3. 3. 오늘말하고싶은것 1. Vue컴포넌트테스트작성방법
 2. 테스트를작성하면서했던생각들 3.FE파트안에서관련주제로나눈대화
  4. 4. 발표대상 -Vue에대한기본문법을알고있는개발자 - 간단한유닛테스트작성경험이있는개발자 - Vue컴포넌트테스트는어떻게시작하면좋을지궁금한개발자 -그냥듣고싶은개발자
  5. 5. KakaoforBusiness 카카오비즈니스사용자를위한통합비즈니스플랫폼 Kakao for Business 카카오 비즈니스 사용자를 위한 통합 비즈니스 플랫폼
  6. 6. KakaoforBusiness 카카오비즈니스사용자를위한통합비즈니스플랫폼 Nuxt기반의 Vue컴포넌트로 개발된 프로젝트
 
 어느 날부터 혼자 FE운영 시작! 그러던 어느 날….
  7. 7. 
 기존에구현되어있었던덩치가큰몇몇코드들.
 

  8. 8. 
 기존에구현되어있었던덩치가큰몇몇코드들.
 
 유지보수가계속들어오면서,덩치를키움.
 기존구조를유지하며기능추가를하다보니점점수정하는비용이커짐.

  9. 9. 
 기존에구현되어있었던덩치가큰몇몇코드들.
 
 유지보수가계속들어오면서,덩치를키움.
 기존구조를유지하며기능추가를하다보니점점수정하는비용이커짐.
 나:
 더수정되기전에리팩토링으로(=제눈에익숙한코드로)바꾸고싶어요!
 

  10. 10. 
 기존에구현되어있었던덩치가큰몇몇코드들.
 
 유지보수가계속들어오면서,덩치를키움.
 기존구조를유지하며기능추가를하다보니점점수정하는비용이커짐.
 나:
 더수정되기전에리팩토링으로(=제눈에익숙한코드로)바꾸고싶어요!
 
 P:
 QA를거치고운영되는코드는귀한코드.
 테스트코드없이리팩토링어떻게보장?(가시적으로,안정성면으로)
 테스트코드와함께리팩토링하는게좋을것?
  11. 11. 
 기존에구현되어있었던덩치가큰몇몇코드들.
 
 유지보수가계속들어오면서,덩치를키움.
 기존구조를유지하며기능추가를하다보니점점수정하는비용이커짐.
 나:
 더수정되기전에리팩토링으로(=제눈에익숙한코드로)바꾸고싶어요!
 
 P:
 QA를거치고운영되는코드는귀한코드.
 테스트코드없이리팩토링어떻게보장?(가시적으로,안정성면으로)
 테스트코드와함께리팩토링하는게좋을것? (+기능추가와리팩토링은같이하지말것)
  12. 12. 운영 중인 (귀한) 코드를 리팩토링 할 때 테스트코드가 안정성을 도와주고, 추상적인 진행상황을 가시적으로 측정가능하게 해줌! 테스트를 시작한 이유 1
  13. 13. 운영업무 외의 도전과제.
 (프로젝트 코드를 대상, 컴포넌트 테스트 학습과 적용) 
 테스트가 없던 프로젝트에 환경을 마련하고 
 추후 추진력있게 테스트를 작성할 수 있는 기반 마련. 테스트를 시작한 이유 2
  14. 14. 환경구성 컴포넌트 생성 테스트 컴포넌트 데이터 테스트 컴포넌트 간 테스트
  15. 15. 환경구성
  16. 16. │ … ├─pages (component) │ │ … │ ├─inspection │ │ inspectionDetail.vue │ │ … ├─tests │ ├─mock │ │ approvedDetails.js │ │ rejectedDetails.js │ │ … │ ├─specs │ │ inspectionDetail.spec.vue │ │ … │ │ _setup.js │ │ … 👈 테스트 당할 파일 pages/…/inspectionDetail.vue
  17. 17. │ … ├─pages (component) │ │ … │ ├─inspection │ │ inspectionDetail.vue │ │ … ├─tests │ ├─mock │ │ approvedDetails.js │ │ rejectedDetails.js │ │ … │ ├─specs │ │ inspectionDetail.spec.vue │ │ … │ │ _setup.js │ │ … 👈 테스트 당할 파일 pages/…/inspectionDetail.vue 👈 테스트코드 작성 파일 tests/…/inspectionDetail.spec.js
  18. 18. │ … ├─pages (component) │ │ … │ ├─inspection │ │ inspectionDetail.vue │ │ … ├─tests │ ├─mock │ │ approvedDetails.js │ │ rejectedDetails.js │ │ … │ ├─specs │ │ inspectionDetail.spec.vue │ │ … │ │ _setup.js │ │ … 👈 테스트 당할 파일 pages/…/inspectionDetail.vue 👈 테스트코드 작성 파일 tests/…/inspectionDetail.spec.js 👈 mock data (가짜 response data)
  19. 19. 테스트작성파일셋팅 test('앱정보가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const option = {     mocks: {       $route: {         query: { appId: null, appName: null },       },     }   }   // when   const wrapper = mount(InspectionPage, option)   // then   // ... }) test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const option = {     mocks: {       $route: {         query: { appId: FAKE_APP_ID, appName: FAKE_APP_NAME },       },     }   }   // when   const wrapper = mount(InspectionPage, option)   // then   // ... }) import test from 'ava' import sinon from 'sinon' import { mount, shallowMount } from '@vue/test-utils' import InspectionPage from '~/pages/.../inspectionDetail.vue' import InspectionErrorModal from '~/components/.../InspectionErrorModal.vue' import statesMock from '~/tests/mock/states' import approvedDetailsMock from '~/tests/mock/approvedDetails' import rejectedDetailsMock from '~/tests/mock/rejectedDetails' import {   // 검수 종류   INSPECTION_TYPES,   // 검수 진행상태   WAITING_INSPECTION,   OPEN_INSPECTION,   ... } from '~/../constants' test('...', t => { }) 👇 테스트도구 & 테스트할 컴포넌트
  20. 20. import test from 'ava' import sinon from 'sinon' import { mount, shallowMount } from '@vue/test-utils' import InspectionPage from '~/pages/.../inspectionDetail.vue' import InspectionErrorModal from '~/components/.../InspectionErrorModal.vue' import statesMock from '~/tests/mock/states' import approvedDetailsMock from '~/tests/mock/approvedDetails' import rejectedDetailsMock from '~/tests/mock/rejectedDetails' import {   // 검수 종류   INSPECTION_TYPES,   // 검수 진행상태   WAITING_INSPECTION,   OPEN_INSPECTION,   ... } from '~/../constants' test('...', t => { }) 테스트작성파일셋팅 👆 가짜 Response Data 준비 👈 상수
  21. 21. import test from 'ava' import sinon from 'sinon' import { mount, shallowMount } from '@vue/test-utils' import InspectionPage from '~/pages/.../inspectionDetail.vue' import InspectionErrorModal from '~/components/.../InspectionErrorModal.vue' import statesMock from '~/tests/mock/states' import approvedDetailsMock from '~/tests/mock/approvedDetails' import rejectedDetailsMock from '~/tests/mock/rejectedDetails' import {   // 검수 종류   INSPECTION_TYPES,   // 검수 진행상태   WAITING_INSPECTION,   OPEN_INSPECTION,   ... } from '~/../constants' test('...', t => { }) 테스트작성파일셋팅 👈 여기부터 테스트 코드 작성
  22. 22. 컴포넌트 생성 테스트
  23. 23. 서비스검수신청진입페이지. 시나리오 페이지 진입시 (페이지 컴포넌트 생성) 아이디정보가 없으면, 에러 모달을 보여준다.
  24. 24. 첫번째테스트작성… test('앱정보가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const option = {     mocks: {       $route: {         query: { appId: null, appName: null },       },     }   }   // when   const wrapper = mount(InspectionPage, option)   // then   // ... }) test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const option = {     mocks: {       $route: {         query: { appId: FAKE_APP_ID, appName: FAKE_APP_NAME },       },     }   }   // when   const wrapper = mount(InspectionPage, option)   // then   // ... }) test('아이디정보가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const option = {     mocks: {       $route: {         query: { id: null },       },     }   }   // when   const wrapper = mount(InspectionPage, option)   // then   // ... }) test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const FAKE_ID = 123456   const option = {     mocks: {       $route: {         query: { id: FAKE_ID },       },     }   }   // when   const wrapper = mount(InspectionPage, option)   // then   // ... })
  25. 25. test('아이디정보가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const option = {     mocks: {       $route: {         query: { id: null },       },     }   }   // when   const wrapper = mount(InspectionPage, option)   // then   // ... }) test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const FAKE_ID = 123456   const option = {     mocks: {       $route: {         query: { id: FAKE_ID },       },     }   }   // when   const wrapper = mount(InspectionPage, option)   // then   // ... }) 두번째테스트작성… 💣중복발생💣
  26. 26. function createOption (query) {   const FAKE_ID = 123456   const defaultQuery = { id: FAKE_ID }      return {     mocks: {       $route: {         query: query || defaultQuery       },     },   } } test('아이디정보가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const option = createOption({ id: null })   // ... }) test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const option = createOption()   // ... }) 💣중복제거 💣=>컴포넌트생성조건옵션유틸추가
  27. 27. import InspectionPage from '~/pages/.../inspectionDetail.vue' test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const EMPTY_RESPONSE = null   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => EMPTY_RESPONSE)   // when   const option = createOption()   const wrapper = mount(InspectionPage, option)   // then   t.true(requestInspectionStateStub.calledOnce)   t.is(wrapper.vm.showErrorModal, true) }) test('검수상태 조회결과가 [심사 대기상태] 일 경우, 페이지 진입시 에러모달을 보여준다.', t => {   // given   const MOCK_WAITING_STATE = statesMock[WAITING_INSPECTION]   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => MOCK_WAITING_STA TE)   // when   const option = createOption()   const wrapper = mount(InspectionPage, option)   // then   t.true(requestInspectionStateStub.calledOnce)   t.is(wrapper.vm.showErrorModal, true) }) 그다음은,mount되기전로직테스트
  28. 28. import InspectionPage from '~/pages/.../inspectionDetail.vue' test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const EMPTY_RESPONSE = null   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => EMPTY_RESPONSE)   // when   const option = createOption()   const wrapper = mount(InspectionPage, option)   // then   t.true(requestInspectionStateStub.calledOnce)   t.is(wrapper.vm.showErrorModal, true) }) test('검수상태 조회결과가 [심사 대기상태] 일 경우, 페이지 진입시 에러모달을 보여준다.', t => {   // given   const MOCK_WAITING_STATE = statesMock[WAITING_INSPECTION]   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => MOCK_WAITING_STA TE)   // when   const option = createOption()   const wrapper = mount(InspectionPage, option)   // then   t.true(requestInspectionStateStub.calledOnce)   t.is(wrapper.vm.showErrorModal, true) }) 그다음은,mount되기전로직테스트=>testdouble활용
  29. 29. import InspectionPage from '~/pages/.../inspectionDetail.vue' test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const EMPTY_RESPONSE = null   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => EMPTY_RESPONSE)   // when   const option = createOption()   const wrapper = mount(InspectionPage, option)   // then   t.true(requestInspectionStateStub.calledOnce)   t.is(wrapper.vm.showErrorModal, true) }) test('검수상태 조회결과가 [심사 대기상태] 일 경우, 페이지 진입시 에러모달을 보여준다.', t => {   // given   const MOCK_WAITING_STATE = statesMock[WAITING_INSPECTION]   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => MOCK_WAITING_STA TE)   // when   const option = createOption()   const wrapper = mount(InspectionPage, option)   // then   t.true(requestInspectionStateStub.calledOnce)   t.is(wrapper.vm.showErrorModal, true) }) mount되기전로직테스트두번째작성
  30. 30. import InspectionPage from '~/pages/.../inspectionDetail.vue' test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const EMPTY_RESPONSE = null   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => EMPTY_RESPONSE)   // when   const option = createOption()   const wrapper = mount(InspectionPage, option)   // then   t.true(requestInspectionStateStub.calledOnce)   t.is(wrapper.vm.showErrorModal, true) }) test('검수상태 조회결과가 [심사 대기상태] 일 경우, 페이지 진입시 에러모달을 보여준다.', t => {   // given   const MOCK_WAITING_STATE = statesMock[WAITING_INSPECTION]   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => MOCK_WAITING_STA TE)   // when   const option = createOption()   const wrapper = mount(InspectionPage, option)   // then   t.true(requestInspectionStateStub.calledOnce)   t.is(wrapper.vm.showErrorModal, true) }) mount되기전로직테스트두번째작성=> 🚨에러발생🚨 앞선테스트에서 (전역)컴포넌트에이미붙여둔테스트더블이겹쳐서… requestInspectionState
  31. 31. 🚨에러해결🚨=>테스트더블복원유틸추가 function restore (...testDoubles) {   testDoubles.forEach(obj => obj.restore && obj.restore()) } test('검수상태 조회 후 응답데이터가 없으면, 페이지 진입시 에러 모달을 보여준다.', t => {   // given   const EMPTY_RESPONSE = null   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => EMPT Y_RESPONSE)   // ...   restore(requestInspectionStateStub) }) test('검수상태 조회결과가 [심사 대기상태] 일 경우, 페이지 진입시 에러모달을 보여준다.', t => {   // given   const MOCK_WAITING_STATE = statesMock[WAITING_INSPECTION]   const requestInspectionStateStub =     sinon.stub(InspectionPage.methods, 'requestInspectionState').callsFake(() => MOCK _WAITING_STATE)   // ...   restore(requestInspectionStateStub) }) Sss
  32. 32. 테스트더블을붙이는시기에따라,restore()필요여부가달랐음. mounted이전의LifeCycleHook으로호출되는로직체크시 전역컴포넌트객체에테스트더블을붙이므로 전역컴포넌트가오염되지않도록 restore()사용이필요했지만, mounted이후에는 컴포넌트인스턴스(vm)에테스트더블을붙일수있고 각Test의스코프마다인스턴스(vm)를생성하므로 restore호출이필요하지않았음.
  33. 33. 테스트더블을붙이는시기에따라,restore()필요여부가달랐음. mounted이전의LifeCycleHook으로호출되는로직체크시 전역컴포넌트객체에테스트더블을붙이므로 전역컴포넌트가오염되지않도록 restore()사용이필요했지만, mounted이후에는 컴포넌트인스턴스에테스트더블을붙일수있고 각Test의스코프마다인스턴스를생성하므로 restore()호출이필요하지않았음.
  34. 34. 🙄리팩토링+테스트전략 🙄 LifeCycleHook안의코드를 쉽게테스트하려면? • 추상화된메서드들을호출하는코드만있는게좋지않을까? • 👉호출시점이프레임워크에서제어되고, • 👉테스트더블을붙여테스트하기수월 • 컴포넌트곳곳에 hook메서드내에서추상화되지않은코드들이꽤있었는데 • 👉보일때마다리팩토링을해야겠다고생각
  35. 35. 컴포넌트 데이터 테스트
  36. 36. { • props • data • computed 
 데이터를변경시키는… • (watch) • (methods) 테스트 해야 할 데이터 종류
  37. 37. { • <template>표현식결과값 • props • data • computed 데이터를변경시키는… • (watch) • (methods) 테스트 해야 할 데이터 종류
  38. 38. 1)템플릿에표현식이있을때 <!-- 1: 템플릿에 로직이 있음 --> <div   ref="headline"   v-if=“types.includes(INSPECTION_TYPES.FOO)">   <h4 class="title">타이틀</h4>   <p class="desc">디스크립션</p> </div> <!-- 2: 템플릿에 데이터만 바인딩 --> <div v-if="hasAType">   <h4 class="title">타이틀</h4>   <p class="desc">디스크립션</p> </div>
  39. 39. <!-- 1: 템플릿에 로직이 있음 --> <div   ref="headline"   v-if=“types.includes(INSPECTION_TYPES.FOO)">   <h4 class="title">타이틀</h4>   <p class="desc">디스크립션</p> </div> <!-- 2: 템플릿에 데이터만 바인딩 --> test('선택한 검수타입에 [Foo]가 있으면, Foo 헤드라인을 보여준다.', t => {   // ...   // then   const headline = wrapper.find({ ref: 'headline' })   const headlineHtml = headline.html()   t.true(headlineHtml.includes('<h4 class="title">타이틀</h4>')) }) 1)템플릿에표현식이있을때,render결과물을테스트 테스트 할 dom 셀렉터 필요
  40. 40. 2)템플릿표현식을제거하면, <!-- 2: 템플릿에 데이터만 바인딩 --> <div v-if="hasFooType">   <h4 class="title">타이틀</h4>   <p class="desc">디스크립션</p> </div> computed 속성 활용
  41. 41. 2)템플릿표현식을제거하면,치환된computed결과값을테스트 <!-- 2: 템플릿에 데이터만 바인딩 --> <div v-if="hasFooType">   <h4 class="title">타이틀</h4>   <p class="desc">디스크립션</p> </div> test('선택한 검수타입에 [Foo]가 있으면, Foo 헤드라인을 보여준다.', t => {   // ...   // then   t.is(wrapper.vm.hasFooType, true) }) computed 속성 활용
  42. 42. 수다 중에 PO 관련 내용이 나와서 공유를 드려볼께요.
 
 PO (Page Object) 관련 내용입니다. https://martinfowler.com/bliki/PageObject.html🐨
  43. 43. 근래에 자주 이야기하는 mvvm에서 view template 이 로직을 나누어 가지지 말자는 이야기도 이것과 통하는 이야기라서 덧붙이자면. 테스트 코드가 view를 그리는 레벨과 최대한 직접 관계를 맺지 않는 장치에요. 수다 중에 PO 관련 내용이 나와서 공유를 드려볼께요.
 
 PO (Page Object) 관련 내용입니다. https://martinfowler.com/bliki/PageObject.html view 의 변화율은 애플리케이션에서 다른 무엇보다 변화율이 높아서. 프레임웍의 refs 를 통해서 view에 접근하듯이 PO를 중간에 두자는 관점이에요 🐨 🐨 🐨 🐨
  44. 44. 그래서. 템플릿에 :if="isRight && isNotLeft, ..." 처럼 view 를 관여하는 로직이 포함되어 있다면 어서 제거를...🐨
  45. 45. (찔림) 자세히는 처음 알았어여 감사합니당 그래서. 템플릿에 :if="isRight && isNotLeft, ..." 처럼 view 를 관여하는 로직이 포함되어 있다면 어서 제거를... 바쁘시겠지만 아지트나 위키에 기록해주실수 있나요? 누군가 언젠가 필요할때 찾아볼수 있게요 네. 까먹지 말고 체크해둘께요. 👍👍👍👍👍👍👍👍👍 🐨 🐱 🐸 🐨
  46. 46. 🙄리팩토링+테스트전략 🙄 • 리팩토링 • 👉템플릿에서로직(표현식)을분리(computed,methods활용) • 테스트전략 • 👉DOM에접근해(ex.ref,class,id)렌더링된결과물을확인하기보다는, • 👉컴포넌트데이터를확인하자. • 👉컴포넌트데이터의템플릿바인딩여부는snapshot이나e2e에게책임을.
  47. 47. 간략하게..snapshot테스팅동작방식 [출처] https://medium.com/@luisvieira_gmr/snapshot-testing-react-components-with-jest-3455d73932a4
  48. 48. 간략하게..snapshot테스팅동작방식 test('선택한 검수타입에 [Foo]가 있으면, Foo 헤드라인을 보여준다.', t => {   // given   const wrapper = mount(InspectionPage, createOption())   // ...   t.snapshot(wrapper.html(), 'Foo가 있을 때 html') }) │ │ … ├─tests │ ├─snapshot │ │ approvedDetail.spec.js.md │ │ approvedDetail.spec.js.snap │ │ … │ ├─specs │ │ inspectionDetail.spec.vue │ │ …
  49. 49. # Snapshot report for `tests/…/inspectionDetaill.spec.js` The actual snapshot is saved in `inspectionDetail.spec.js.snap`. Generated by [AVA](https://ava.li). ## 선택한 검수타입에 [Foo]가 있으면, Foo 헤드라인을 보여준다. > Foo가 있을 때 html     `<div id="article"><div class="title"><h3 class="center">검수 신청하기</ h3><p class=“desc_bizcenter"> …생략…생략 …생략…생략 저장된snapshot │ │ … ├─tests │ ├─snapshot │ │ approvedDetail.spec.js.md │ │ approvedDetail.spec.js.snap │ │ … │ ├─specs │ │ inspectionDetail.spec.vue │ │ …
  50. 50. 저장된snapshot │ │ … ├─tests │ ├─snapshot │ │ approvedDetail.spec.js.md │ │ approvedDetail.spec.js.snap │ │ … │ ├─specs │ │ inspectionDetail.spec.vue │ │ …
  51. 51. { • <template>표현식결과값 • props • data • computed 데이터를변경시키는… • (watch) • (method) const TITLE = '테스트 타이틀' const DESCRIPTION = '테스트 디스크립션' // mount시 props 주입 const wrapper = mount(Component, {   propsData: {     title: TITLE   } }) wrapper.setData({ description: DESCRIPTION }) // props 확인 t.is(wrapper.props('title'), TITLE) // computed 확인 (data와 동일) t.is(wrapper.vm.hasTitleAndDescription, true) 뷰인스턴스생성시,주입
  52. 52. { • <template>표현식결과값 • props • data • computed 데이터를변경시키는… • (watch) • (method) 뷰인스턴스생성시,주입 뷰인스턴스생성후,조작 const TITLE = '테스트 타이틀' const DESCRIPTION = '테스트 디스크립션' // mount시 props 주입 const wrapper = mount(Component, {   propsData: {     title: TITLE   } }) wrapper.setData({ description: DESCRIPTION }) // props 확인 t.is(wrapper.props('title'), TITLE) // computed 확인 (data와 동일) t.is(wrapper.vm.hasTitleAndDescription, true)
  53. 53. { • <template>표현식결과값 • props • data • computed 데이터를변경시키는… • (watch) • (methods) 뷰인스턴스생성시,주입 뷰인스턴스생성후,조작 - 데이터변화확인 - 메서드호출,반환값확인
  54. 54. (나는망각의동물…🙈)
  55. 55. 컴포넌트 간 테스트
  56. 56. mountvsshallowMount [출처] https://vuejsdevelopers.com/2019/09/30/stubs-vue-unit-test/
  57. 57. mountvsshallowMount(렌더링결과비교) <!-- 1. mount의 렌더링 결과값 --> <div>   <div class="bg"></div>   <div class="layer">     <div class="inner_layer" style="top: 300px;">       <div class=“head"> <strong class="title">에러모달 타이틀</strong> </div>       <div class="body">         … </div>     </div>   </div> </div> <!-- 2. shallowMount의 렌더링 결과값 --> <inspectionerrormodal-stub></inspectionerrormodal-stub>
  58. 58. 특정자식컴포넌트만선택해서테스트하려면? // 부모가 자식에게 test(‘페이지에서 메시지를 전달하며 에러모달을 열면, 모달컴포넌트에 prop으로 전달된다.', t => {   // given   const MODAL_MSG = '테스트용 메세지'   const wrapper = shallowMount(InspectionPage, createOption())   // when   wrapper.vm.showErrorModal(MODAL_MSG)   const errorModalWrapper = wrapper.find(InspectionErrorModal)   // then   t.is(errorModalWrapper.props('msg'), MODAL_MSG) }) // 자식이 부모에게 test(‘페이지에서 열린 에러모달에서, show이벤트가 오면 값이 페이지에 업데이트 된다.', t => {   // given   const MODAL_MSG = '테스트용 메세지'   const wrapper = shallowMount(InspectionPage, createOption())   // when   wrapper.vm.showErrorModal(MODAL_MSG)   const errorModalWrapper = wrapper.find(InspectionErrorModal)   errorModalWrapper.vm.$emit('update:show', false)   // then   t.is(wrapper.vm.showInspectionErrorModal, false) })
  59. 59. 부모가자식에게props를전달할때 // 부모가 자식에게 test(‘페이지에서 메시지를 전달하며 에러모달을 열면, 모달컴포넌트에 prop으로 전달된다.', t => {   // given   const MODAL_MSG = '테스트용 메세지'   const wrapper = shallowMount(InspectionPage, createOption())   // when   wrapper.vm.showErrorModal(MODAL_MSG)   const errorModalWrapper = wrapper.find(InspectionErrorModal)   // then   t.is(errorModalWrapper.props('msg'), MODAL_MSG) }) // 자식이 부모에게 test(‘페이지에서 열린 에러모달에서, show이벤트가 오면 값이 페이지에 업데이트 된다.', t => {   // given   const MODAL_MSG = '테스트용 메세지'   const wrapper = shallowMount(InspectionPage, createOption())   // when   wrapper.vm.showErrorModal(MODAL_MSG)   const errorModalWrapper = wrapper.find(InspectionErrorModal)   errorModalWrapper.vm.$emit('update:show', false)   // then   t.is(wrapper.vm.showInspectionErrorModal, false) }) props ErrorModal InspectionPage emit
  60. 60. 자식이부모에게emit이벤트를전달할때 // 자식이 부모에게 test(‘페이지에서 열린 에러모달에서, show이벤트가 오면 값이 페이지에 업데이트 된다.', t => {   // given   const MODAL_MSG = '테스트용 메세지'   const wrapper = shallowMount(InspectionPage, createOption())   // when   wrapper.vm.showErrorModal(MODAL_MSG)   const errorModalWrapper = wrapper.find(InspectionErrorModal)   errorModalWrapper.vm.$emit('update:show', false)   // then   t.is(wrapper.vm.showInspectionErrorModal, false) }) props ErrorModal InspectionPage emit
  61. 61. 과정을통해 얻은것과아쉬운점
  62. 62. 기술적으로… 💻 1. 테스트역할의이해:리팩토링안정성,가시성 2. 컴포넌트테스트방법과노하우 3. 테스트가쉬운컴포넌트고민->근거있는리팩토링->코드일관성 얻은 것 1
  63. 63. 기술적으로… 💻 1. 테스트역할의이해:리팩토링안정성,가시성 2. 컴포넌트테스트방법과노하우 3. 테스트가쉬운컴포넌트고민->근거있는리팩토링->코드일관성 얻은 것 1
  64. 64. 기술적으로… 💻 1. 테스트역할의이해:리팩토링안정성,가시성 2. 컴포넌트테스트방법과노하우 3. 테스트가쉬운컴포넌트고민->근거있는리팩토링->코드일관성 얻은 것 1
  65. 65. 개인적으로… . 1. 업무가소강(?)상태일때할수있는일 2. 운영외의업무로성장하는느낌 3. 피드백에대한갈증해소 얻은 것 2
  66. 66. 개인적으로… . 1. 업무가소강(?)상태일때할수있는일 2. 운영외의업무로성장하는느낌 3. 피드백에대한갈증해소 얻은 것 2
  67. 67. 개인적으로… . 1. 업무가소강(?)상태일때할수있는일 2. 운영외의업무로성장하는느낌 3. 피드백에대한갈증해소 얻은 것 2
  68. 68. 아쉬웠던 점 (feat. 아주 현실적인) 🙈 - 진행상황에대한업무가시화(목록,일정)를잘하지못함 (무엇을해야하고,어디까지해야하고,얼마나걸릴지감없음) (감이없어서감으로가시화) 그럼에도 불구하고
  69. 69. 다음스텝, 남은고민.. 👽 - 기존코드를망가뜨리지않으면서테스트와리팩토링을잘병행하는방법 - 컴포넌트와협력하는외부객체에대한테스트 - 테스트코드의유지보수경험 - 더복잡한케이스의테스트…
  70. 70. 다음스텝, 남은고민.. 👽 - 기존코드를망가뜨리지않으면서테스트와리팩토링을잘병행하는방법 - 컴포넌트와협력하는외부객체에대한테스트 - 테스트코드의유지보수경험 - 더복잡한케이스의테스트…
  71. 71. 다음스텝, 남은고민.. 👽 - 기존코드를망가뜨리지않으면서테스트와리팩토링을잘병행하는방법 - 컴포넌트와협력하는외부객체에대한테스트 - 테스트코드의유지보수경험 - 더복잡한케이스의테스트…
  72. 72. 감사합니다 😺 lumi.kim@kakaocorp.com
  73. 73. Reference - vue-test-util 공식가이드: https://vue-test-utils.vuejs.org - vue-test-util 개발자 블로그: https://eddyerburgh.me - https://joshua1988.github.io/vue-camp/testing/getting-started.html - vue testing handbook: https://lmiller1990.github.io/vue-testing-handbook/ - ava: https://github.com/avajs/ava
  74. 74. Q&A

×