테스트 어플리케이션 (Testing Applications) 1/2

Minho Yoo·2023년 1월 4일
1
post-thumbnail

테스트 코드가 필요한 이유

개발자에게 테스트 코드가 필요한 이유는 아래 2가지에 소모되는 시간을 줄이기 위해서이다.

  1. 방금 구현한 기능이 잘 돌아가는지 확인하는 시간
  2. 특정 기능을 변경했을 때 기존에 있던 다른 기능을 깨트리는지 확인하는 시간

애플리케이션이 커지면 커질수록 위 시나리오를 점검하는데 많은 시간을 소요한다.
특히 진행되고 있는 프로젝트에 투입되어서 남이 짠 코드를 변경한다고 했을 때, 코드를 변경하는 시간보다 그 코드가 다른 코드에 악영향을 주는지 분석하고 확인하는데 시간이 더 많이 들어간다.

테스트 코드는 이러한 시간을 줄여주고 개발자의 자신감을 높여준다.
테스트 코드가 많으면 많을수록 어플리케이션의 안정성이 더욱 높아진다.

무엇을 어떻게 테스트 할 것인가?

테스트 코드를 작성할 때 가장 중요한 것은 무엇을 어떻게 테스트 할 것인가 정하는 것이다.

테스트 코드의 목적은 구현된 코드의 흐름이나 로직을 확인하는 것이 아니라 사용자의 관점에서 버튼 클릭과 키 입력 등의 이벤트에 따라 UI가 올바르게 전개되는지 확인하는 것이다.
예를 들어 아래와 같은 코드가 있다고 해보자.

<template>
  <div>
    <button @click="addCounter">add</button>
    <p>{{ counter }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
  	  counter: 0,
    }
  },
  methods: {
    addCounter() {
  	  this.counter = this.counter + 1;
    }
  }
}
</script>

위 코드는 add 버튼을 클릭했을 때 counter 값을 1씩 증가시키는 코드이다.
아마 테스트 코드가 따로 없다고 가정한다면 대부분 버튼을 클릭해보고 counter 값이 증가하는지 확인할 것이다.

여기서 테스트 코드를 적용한다고 했을 때 다음 2가지 시나리오를 점검할 수 있다.

  1. add 버튼을 클릭했을 때 'counter' 값이 증가했는지 확인
  2. add 버튼을 클릭했을 때 증가된 'counter'값이 화면에 출력되는지 확인

위 2개 시나리오 중 사용자 관점에서 작성된 테스트 시나리오는 2번이다.
1번은 단순히 코드 관점에서 메서드가 올바르게 동작하는지 확인하는 수준이다.

이처럼 테스트 코드는 개발자가 구현한 로직이나 코드 결과를 검증하는 것이 아니라 특정 이벤트에 의해 변경될 UI를 검증할 수 있어야 한다.

테스팅 도구

테스팅 도구는 요구 사항 변경에 따른 기능 추가 및 리팩토링을 할 때 소프트웨어의 안정성을 높여주는 도구이다.
복잡한 웹 애플리케이션일수록, 그리고 같이 협업하는 팀원이 많을수록 테스트 케이스를 바탕으로 각자 기능을 구현하면 기능 변경에서 오는 에러를 미연에 방지할 수 있다.

시중에서 가장 많이 사용되는 테스트 도구는 다음과 같다.

여기서 우리가 배울 테스트 도구는 Jest이다.

Jest 소개

제스트(Jest)는 페이스북에서 만든 자바스크립트 테스팅 라이브러리이다.
테스트 코드의 모양이 직관적이고 문서화가 잘되어 있어 요즘 많이 활용하고 있다.

라이브러리 설치

제스트는 NPM으로 아래와 같이 설치한다.

npm install --save-dev jest

테스트 파일 생성

  • 파일 위치: 테스트 할 파일이 있느 폴더 내
  • 폴더 이름: test
  • 파일 이름: 파일 이름.test.js 또는 파일 이름.spec.js

테스트 파일 경로 설정

테스트 파일은 최대한 테스트 하려는 파일과 가깝게 있는 것이 좋다.
프로젝트 소스 폴더 내의 테스트 파일을 모두 대상으로 테스트를 실행하려면 제스트 설정 파일에 아래와 같은 속성을 추가한다.

// jest.config.js
module.exports = {
  testMatch: ['**/*.spec.[jt]s?(x)', '**/*.test.[jt]s?(x)'],
}

테스트 코드 실행

제스트를 설치하고 나면 콘솔 창에 아래와 같은 명령어로 실행할 수 있다.

jest

위의 명령어를 입력하면 프로젝트 내부에 test.js 또는 spec.js 확장자를 가지는 파일을 모두 실행한다.

TIP

NPM 커스텀 명령어나 npm t로 테스트를 실행할 수도 있다.

테스트 코드 예시

// helloworld.test.js
const str = 'Hello World';

test('HelloWorld Component', () => {
  expect(str).toBe('Hello World');
})

위 코드는 str 의 값이 Hello World가 맞는지 확인하는 테스트 코드이다.
일반적으로 테스트 코드에는 기대값과 결과값이 필요한데 위 코드처럼 expect() 에 기대값을 넣고, toBe() 에 예상 결과값을 넣으면 된다.

이제 콘솔에 jest 명령어를 실행하면 다음과 같은 테스트 결과가 나온다.

helloworld.test.js 파일에서 1개의 테스트 코드를 돌려서 성공했다는 로그이다.

Jest API

Jest로 단위 테스트 코드를 구현할 때 자주 사용되는 API 목록이다.

  • describe()
  • test()
  • expect()
  • beforeEach()

describe()

여러 개의 test() 코드를 하나의 테스트 작업 단위로 묶어주는 API, 하나의 테스트 케이스를 test() 라고 한다면 describe()는 여러 개의 테스트 케이스를 하나의 그룹으로 묶어주는 역할.

describe('Testing 1', () => {
  test('message equals to be Vue', () => {
    // ..
  });
  
  test('data equals to be Object', () => {
    // ..
  });
});

test()

테스트 코드를 돌리기 위한 API. 하나의 테스트 케이스를 의미하며 it()과 같은 역할.

test('message equals to be Vue', () => {
  // ..
});

expect()

테스트 할 대상을 넣는 API. expect() 에는 주로 테스트 입력 값 또는 기대 값을 넣는다.

var message = 'Vue';
test('message equals to be Vue', () => {
  expect(message).toBe('Vue');
});

toBe()

테스트의 결과를 확인하는 API. 테스트의 예상 결과 값을 넣는다.

var message = 'Vue';
test('message equals to be Vue', () => {
  expect(message).toBe('Vue')
})

TIP

테스트 결과 값 API에는 toBe() 뿐만 아니라 toHaveBeenCalled(), toBeTruthy(), toBeFalsy() 등 여러 유형이 있다.
자세한 건 Jest-Expect API 에서 확인.

beforeEach()

테스트 파일의 각 테스트 코드가 돌기 전에 수행할 로직을 넣는 API. 테스트 케이스마다 반복되는 로직을 넣기에 적합.

var message;
beforeEach(() => message = 'Vue');

test('message equals to be Vue', () => {
  expect(message).toBe('Vue');
});

test('message equals to be Vue!!', () => {
  expect(message).toBe('Vue');
});

Jest의 뷰 컴포넌트 테스팅

지금까지 제스트에 대해서 가볍게 살펴봤으니 이번엔 제스트로 뷰 컴포넌트를 테스트해보겠다.
아래는 제스트로 뷰 컴포넌트를 테스트하는 코드이다.

<!-- HelloWorld.vue -->
<template>
  <div>Hello {{ message }}</div>
</template>

<script>
export default {
  data() {
    return {
  	  message: 'Vue!',
    }
  }
}
</script>
// helloworld.test.js
import HelloWorld from './HelloWorld.vue';

test('HelloWorld Component', () => {
  expect(true).tobe(true);
});

위의 테스트 코드는 제스트에서 HelloWorld 컴포넌트를 인식할 수 있는지 확인하는 코드이다.
테스트의 기대 값과 결과 값은 true로 일치하기 때문에 HelloWorld 컴포넌트만 잘 들고온다면 실패 없이 테스트가 완료된다.

하지만, 실제로 실행해보면 테스트가 실패하면서 아래와 같은 결과가 나온다.

위 메세지는 '뷰 컴포넌트는 제스트가 해석할 수 있는 유형의 파일이 아니다'라는 의미이다.
생각해보면 싱글 파일 컴포넌트가 웹팩의 뷰 로더로 해석되어서 웹 자원으로 최종 변환되기 때문에 제스트도 이렇게 변환해주는 도구가 필요하다.

Vue Test Utils

뷰 테스트 유틸(Vue Test Utils)은 코어 팀 멤버가 제작한 테스팅 보조 라이브러리이다.
제스트(Jest) 뿐만 아니라 다른 테스트 도구도 사용할 수 있다.

최신 Vue-CLI(3.x 버전 이상)에서 설치 방법

새로운 프로젝트의 경우 기본 환경이 설정된 기본 프리셋(Default)을 사용할 수 있지만, 뷰 테스트 유틸을 설치하기 위해 아래와 같이 Manually select features 옵션을 선택해준다.


뷰 테스트 유틸을 설치하기 위해 Unit Testing 옵션을 선택해준다.

TIP

방향키(↑, ↓)로 항목을 이동할 수 있고 space 키로 선택/해제, enter 키로 결정할 수 있습니다.

Vue CLI 옵션 선택

Unit Testing 옵션을 추가하고 나면 아래와 같이 차례대로 선택한다.
1. 먼저 코드 정리 도구인 Prettier와 문법 검사 도구인 ESLint를 선택한다.

  1. 다음은 문법 검사 도구의 실행 시점을 선택한다. 에디터에서 저장을 누를 때마다 검사하는 것으로 선택한다.

  2. 단위 테스트 도구는 제스트로 선택한다.

  3. 위에서 추가한 ESLint와 프리티어의 설정 내용을 package.json에 추가하지 않고 개별 설정 파일에 관리한다.

기존 Vue-CLI (2.x 버전 이하)에서 설치 방법

아래의 명령어로 뷰 테스트 유틸 라이브러리를 설치한다.

npm install jest @vue/test-utils vue-jest babel-jest --save-dev

위 명령어로 vue test util, jest, vue-jest, babel-jest 4개의 라이브러리가 설치된다.

babel 사용하기

프로젝트에 바벨(babel)을 설정한 적이 없다면 아래와 같이 설치해준다.

npm install @babel/core @babel/preset-env babel-core@^7.0.0-bridge.0 --save-dev

바벨은 자바스크립트(JavaScript) 컴파일러로서 작성된 최신 코드(ECMAScript 2015 버전 이상)를 이전 버전(오래된 브라우저 또는 환경)에 호환하여 동작할 수 있도록 코드를 변환해주는 도구이다.
예를 들어 아래 코드와 같이 ES2015+로 작성된 문법을 이전 자바스크립트 문법으로 변환해 준다.

// 바벨 입력: ES2015 화살표 함수
[1, 2, 3].map(n => n + 1);

// 바벨 출력: 변환된 코드
[1, 2, 3].map(function(n) => {
	return n + 1;
})

바벨 프리셋을 package.json 또는 babel.config.json 또는 babel.config.js에서 설정할 수 있다.

// package.json
{
  // ...
  "babel": {
  	"presets": ["@babel/preset-env"]
  }
}
// babel.config.json
{
  "presets": ["@babel/preset-env"]
}
// babel.config.js
module.exports = {
  presets: ["@babel/preset-env"], // 수동 설정
  presets: ["@vue/cli-plugin-babel/preset"] // vue cli로 설치한 경우 자동 설정됨
}

웹팩(Webpack) 별칭(Alias) 사용

// package.json
{
  "jest": {
    "moduleNameMapper": {
      // 별칭 @(프로젝트/src) 사용하여 하위 경로의 파일을 맵핑한다.
      "^@/(.*)$": "<rootDir>/src/$1"
    }
  }
}
// jest.config.js
module.exports = {
  moduleNameMapper: {
    // 별칭 @(프로젝트/src) 사용하여 하위 경로의 파일을 맵핑한다.
    '^@/(.*)$': '<rootDir>/src/$1'
  },
};

프로젝트 경로/src 까지 경로 @ 별칭으로 맵핑한다.

// '프로젝트 경로/src/' 하위에 존재하는 파일을 아래와 같이 간소화하여 작성할 수 있다.
import HelloWorld from "@/components/HelloWorld.vue";

코드 커버리지(Code Coverage)

제스트는 테스트의 성공, 실패 개수를 나타내는 결과뿐만 아니라 테스트 커버리지를 나타내는 지표 보고서도 생성할 수 있다.

아래와 같이 제스트 환경 설정에 적용한 후

// packages.json
{
  "jest": {
  	"collectCoverage": true,
    "collectCoverageFrom": [
      "**/*.{js, vue}",
      "!**/node_modules/**"
    ]
  }
}

테스트를 실행해보면 터미널에 아래와 같이 표 형식으로 결과를 보여준다.

PASS  tests/unit/example.spec.js
  HelloWorld.vue
    √ renders props.msg when passed (14ms)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |        0 |        0 |        0 |        0 |                   |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.628s
Ran all test suites.
Done in 2.46s.

각 커버리지 항목의 설명

  • Stmts: 최소 한 번 이상 이상 실행된 명령문(변수에 값 저장, 함수 호출 등) 코드의 비율
  • Branch: 최소 한 번 이상 if, switch와 같은 분기 조건이 충족된 비율
  • Funcs: 최소 한 번 이상 호출된 함수의 비율
  • Lines: 최소 한 번 이상 실행된 코드 라인의 비율
  • Uncovered Line: 코드 커버리지에 측정되지 않은 코드 라인 수

제스트 환경 설정

package.json 에서 설정 하거나 jest.config.js 에서 설정 할 수 있다.

package.json 설정

{
  // ...
  "jest": {
    // vue-cli 테스트 환경 설정을 사용한다
    // 주의! preset 지정 후 아래와 같이 각각 다시 설정하는 경우, 새로 설정한 내용으로 적용된다
    "preset": "@vue/cli-plugin-unit-jest",
    "moduleFileExtensions": [
      "js",
      "json",
      // 모든 vue 파일(`*.vue`)을 처리하기 위해 Jest에게 알려준다
      "vue"
    ],
    "transform": {
      // `vue-jest`를 사용하여 모든 vue 파일(`*.vue`)을 처리한다
      ".*\\.(vue)$": "vue-jest",
      // `babel-jest`를 사용하여 모든 js 파일(`*.js`)을 처리한다
      ".*\\.(js)$": "babel-jest",
    },
    "moduleNameMapper": {
      // 별칭 @(프로젝트/src) 사용하여 하위 경로의 파일을 맵핑한다
      "^@/(.*)$": "<rootDir>/src/$1"
    },
    "testMatch": [
      // __tests__ 경로 하위에 있는 모든 js/ts/jsx/tsx 파일을 테스트 대상으로 지정한다
      "**/__tests__/**/*.[jt]s?(x)",
      // 파일 이름에 'xxx.spec' 또는 'xxx.test'라는 이름이 붙여인 모든 js/ts/jsx/tsx 파일을 테스트 대상으로 지정한다
      "**/?(*.)+(spec|test).[jt]s?(x)"
    ],
    // node_modules 경로 하위에 있는 모든 테스트 파일을 대상에서 제외한다
    "testPathIgnorePatterns": ["/node_modules/"],
    "collectCoverage": true,
    "collectCoverageFrom": [
      "**/*.{js,vue}",
      "!**/node_modules/**"
    ]
  }
}

WARNING

JSON 파일이므로 복사해 붙여넣을 때 주석은 제거해야됨

jest.config.js 설정

Vue-CLI를 이용하여 Unit Testing을 선택했다면 jest.config.js 파일을 자동으로 생성한다.
npm으로 직접 설치했거나, 아직 파일이 존재하지 않는다면 프로젝트 경로(최상위)에 jest.config.js 파일을 생성해준다.

module.exports = {
  // (vue-cli로 설치 시 기본 세팅됨) vue-cli 테스트 환경 설정을 사용한다
  // 주의! preset 지정 후 아래와 같이 각각 다시 설정하는 경우, 새로 설정한 내용으로 적용된다
  preset: "@vue/cli-plugin-unit-jest",
  moduleFileExtensions: [
    'js',
    'json',
    // 모든 vue 파일(`*.vue`)을 처리하기 위해 Jest에게 알려준다
    'vue',
  ],
  transform: {
    // `vue-jest`를 사용하여 모든 vue 파일(`*.vue`)을 처리한다
    '.*\\.(vue)$': 'vue-jest',
    // `babel-jest`를 사용하여 모든 js 파일(`*.js`)을 처리한다
    '.*\\.(js)$': 'babel-jest',
  },
  moduleNameMapper: {
    // 별칭 @(프로젝트/src) 사용하여 하위 경로의 파일을 맵핑한다
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  testMatch: [
    // __tests__ 경로 하위에 있는 모든 js/ts/jsx/tsx 파일을 테스트 대상으로 지정한다
    '**/__tests__/**/*.[jt]s?(x)',
    // 'xxx.spec' 또는 'xxx.test'라는 이름의 모든 js/ts/jsx/tsx 파일을 테스트 대상으로 지정한다
    '**/?(*.)+(spec|test).[jt]s?(x)'
  ],
  // node_modules 경로 하위에 있는 모든 테스트 파일을 대상에서 제외한다
  testPathIgnorePatterns: ['/node_modules/'],
  collectCoverage: true,
  collectCoverageFrom: [
    '**/*.{js,vue}',
    '!**/node_modules/**'
  ],
};

TIP

jest.config.js 파일로 분리하면 환경 설정 부분만 모아놓을 수 있어서 유지보수가 쉬워진다.
자바스크립트 파일이므로 주석 작성도 가능하다.

profile
Always happy coding 😊

0개의 댓글