현대 프로그래밍에 있어서 빼놓을 수 없는 것이 TDD를 이용한 생산성 향상인데, 이번 포스팅에서는 NUXT프로젝트에 이를 도입하기 위해서 jest 를 이용해 간단한 TDD 패턴을 한바퀴 돌려보자.
Test Driven Development의 약자로 우리말로는 테스트 주도 개발 이라고 한다.
일반적인 개발 작업은 요구사항을 생각하고 코딩 해보고 돌아가는지 테스트 한다. (그리고 몇배나 긴시간을 대부분 디버깅에 건다.)
즉, 테스트/디버그가 가장 마지막 단계 이고 그나마도 자동화 되어 있지 않아서 예상시간보다 더 길게 작업 하기 일쑤이며 평소엔 잘 되던게 막상 데모날 안된다던가 하는 경험이 다들 한두번은 있을 것이다.
이러한 문제점을 해소하고 항상 기대되는 동작을 보장 하기 위해 TDD는 역순서로 요구사항에 맞게 자동화테스트를 먼저 짜고 그 테스트를 FAIL에서 SUCCESS로 만들어 나가는 개발방법론이다. 모든 테스트가 성공 할때만 PR(pull request)가 머지 된다던가 하는 식으로 자동화를 해서 jenkins등 CI/CD 구축 을 한다면 생산성과 신뢰성은 비약적으로 상승한다.
예를 들자면, sum(a,b)
라는 함수가 있고 두 수를 더한 값을 리턴 해주는게 요구사항 이라면 expect(sum(10,20)).toBe(30)
과 같은 테스트코드를 먼저 작성 하고 이를 PASS 시키는 순서로 작업 해나간다.
한번 작성한 테스트는 영원히 남아 있기 때문에 이후 다른 작업에서 side-effect가 발생 하는 경우 즉시 눈치 채고 디버깅을 할 수 있다.
단점이라면, 커버리지(Coverage)에 들어가지 않는 예외상황에는 유연하게 대처가 안 될 수 있으며 이 커버리지를 100%로 잡으려고 하면 실제 개발보다 테스트코드 짜는데 소요되는 시간비용이 훨씬 더 커지는, 소위 배보다 배꼽이 큰 상황이 올 수도 있다는 것이다. 그리고 혹여나 사양/기획이 변경 된다면 리팩토링 양도 곱절이 된다. ㅠㅠ
Javascript(자바스크립트) 위에서 만들어진 테스팅 프레임워크로 Babel, TypeScript, Node, React, Angular, Vue 등에 널리 사용 되고 있다. 심플함을 모토로 개발 되었다. 코드가 올바르게 동작 하는지 검사하는 유틸리티들을 제공 한다. 이를테면 toBe()
, toEqual()
, toThrowError()
등 과 같은 함수들 이다. 테스트가 실패 하면 왜 실패하는지 최대한 힌트를 제공하기 때문에 매우 유용하다.
초보자의 관점에서 넉스트(nuxt.js, vue.js) 프로젝트에 제스트를 이용 해서 간단한 테스트 '헬로 월드'를 작성 하는 방법을 알아보자.
우선 넉스트 프로젝트를 생성 한다.
yarn create nuxt-app myjest
프로젝트명은 편한데로 지으면 된다. 여기서는 myjest로 정했다.
뒤이어 자동 실행되는 Nuxt 프로젝트 생성 헬퍼에서 다른건 자유롭게 선택 하면 되는데 Testing Framework
를 고를때 Jest
를 선택 해주는 것이 중요하다. 이렇게 하면 귀찮은 추가작업 없이 바로 테스트를 이용 할 수 있다. 만약 기존 프로젝트에 테스트를 도입 하고자 한다면 yarn add jest
커맨드를 통해서 설치 하는 것도 가능 하다.
필자가 선택한 옵션들
yarn create v1.22.10
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Installed "create-nuxt-app@3.7.1" with binaries:
- create-nuxt-app
create-nuxt-app v3.7.1
✨ Generating Nuxt.js project in myjest
? Project name: myjest
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git
프로젝트가 잘 생성 되면 cd myjest
커맨드를 이용해 프로젝트 폴더로 이동 하자. 그리고 yarn test
커맨드를 입력시 디폴트로 작성 되어 있는 테스트가 실행 되고 커버리지 리포트까지 출력 되는 것을 확인 할 수 있다.
yarn test
커맨드는 package.json
파일에 미리 넣어져 있으며 코드는 아래와 같다.
{
"name": "myjest",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"test": "jest"
},
"dependencies": {
"core-js": "^3.15.1",
"nuxt": "^2.15.7"
},
"devDependencies": {
"@vue/test-utils": "^1.2.1",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^27.0.5",
"jest": "^27.0.5",
"vue-jest": "^3.0.4"
}
}
"test": "jest"
에 주목 하면 된다.
그리고 jest에 대해서도 설정 파일이 존재 하는데 jest.config.js
이며 내용은 아래와 같다.
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
'^~/(.*)$': '<rootDir>/$1',
'^vue$': 'vue/dist/vue.common.js'
},
moduleFileExtensions: [
'js',
'vue',
'json'
],
transform: {
'^.+\\.js$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest'
},
collectCoverage: true,
collectCoverageFrom: [
'<rootDir>/components/**/*.vue',
'<rootDir>/pages/**/*.vue'
],
testEnvironment: 'jsdom'
}
커버리지 리포트가 느리고 불편하다면 여기서 false로 변경 해주면 되겠다.
따로 타겟 테스트 파일을 지정 하지 않는다면 제스트에서는 *.spec.js
접미어로 끝나는 파일과 *.test.js
파일을 찾아서 돌려보게 설계 되어 있다.
샘플로 돌았던 테스트는 /test/NuxtLogo.spec.js
파일에 정의 되어 있으며 코드는 아래와 같다.
import { mount } from '@vue/test-utils'
import NuxtLogo from '@/components/NuxtLogo.vue'
describe('NuxtLogo', () => {
test('is a Vue instance', () => {
const wrapper = mount(NuxtLogo)
expect(wrapper.vm).toBeTruthy()
})
})
테스트 코드를 읽어보자면 NuxtLogo 컴포넌트를 불러와서 마운트 해보고 인스턴스에 해당하는 vm이 존재 해야 한다는 '요구사항' 을 정의 하고 있다. 문법 오류가 있다던지 해서 마운트가 실패한다면 해당 테스트에서 이를 쉽게 발견 할 수 있다.
NuxtLogo.vue 파일은 /components/NuxtLogo.vue
에 정의 되어 있으며 크게 중요 하진 않지만 코드는 아래와 같다.
<template>
<svg class="nuxt-logo" viewBox="0 0 45 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.7203 29.704H41.1008C41.6211 29.7041 42.1322 29.5669 42.5828 29.3061C43.0334 29.0454 43.4075 28.6704 43.6675 28.2188C43.9275 27.7672 44.0643 27.2549 44.0641 26.7335C44.0639 26.2121 43.9266 25.6999 43.6662 25.2485L32.6655 6.15312C32.4055 5.70162 32.0315 5.32667 31.581 5.06598C31.1305 4.8053 30.6195 4.66805 30.0994 4.66805C29.5792 4.66805 29.0682 4.8053 28.6177 5.06598C28.1672 5.32667 27.7932 5.70162 27.5332 6.15312L24.7203 11.039L19.2208 1.48485C18.9606 1.03338 18.5864 0.658493 18.1358 0.397853C17.6852 0.137213 17.1741 0 16.6538 0C16.1336 0 15.6225 0.137213 15.1719 0.397853C14.7213 0.658493 14.3471 1.03338 14.0868 1.48485L0.397874 25.2485C0.137452 25.6999 0.000226653 26.2121 2.8053e-07 26.7335C-0.000226092 27.2549 0.136554 27.7672 0.396584 28.2188C0.656614 28.6704 1.03072 29.0454 1.48129 29.3061C1.93185 29.5669 2.44298 29.7041 2.96326 29.704H13.2456C17.3195 29.704 20.3239 27.9106 22.3912 24.4118L27.4102 15.7008L30.0986 11.039L38.1667 25.0422H27.4102L24.7203 29.704ZM13.0779 25.0374L5.9022 25.0358L16.6586 6.36589L22.0257 15.7008L18.4322 21.9401C17.0593 24.2103 15.4996 25.0374 13.0779 25.0374Z" fill="#00DC82" />
</svg>
</template>
<style>
.nuxt-logo {
height: 180px;
}
</style>
테스트가 통과 되었으므로 컴파일 에러 없이 잘 마운트 된다는 뜻이다.
jest가 잘 동작되는지 확인 해보기 위해서 '일부러' 잘못된 코드를 넣어보자.
4라인의 </svg>
닫힘 태그에 아래처럼 오타로 1을 넣어보자.
그리고 yarn test
를 실행 하면 마운트가 실패함을 알 수 있다.
tag <svg> has no matching end tag.
FAIL test/NuxtLogo.spec.js
● Test suite failed to run
[vue-jest] Error: Vue template compilation failed
at error (node_modules/vue-jest/lib/throw-error.js:2:9)
Running coverage on untested files...Failed to collect coverage from /Users/max/mj/myjest/components/NuxtLogo.vue
ERROR:
[vue-jest] Error: Vue template compilation failed
STACK: Error:
[vue-jest] Error: Vue template compilation failed
at error (/Users/max/mj/myjest/node_modules/vue-jest/lib/throw-error.js:2:9)
at compileTemplate (/Users/max/mj/myjest/node_modules/vue-jest/lib/template-compiler.js:29:5)
at Object.module.exports [as process] (/Users/max/mj/myjest/node_modules/vue-jest/lib/process.js:66:29)
at ScriptTransformer.transformSource (/Users/max/mj/myjest/node_modules/@jest/transform/build/ScriptTransformer.js:612:31)
at _default (/Users/max/mj/myjest/node_modules/@jest/reporters/build/generateEmptyCoverage.js:143:38)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
여기에 출력된 메시지 tag <svg> has no matching end tag.
를 통해서 특정 태그가 열림/닫힘이 매칭 되지 않는다고 상세하게 알려준다. (일부러 넣었던) 오타를 고칠시 즉시 패스 되는 것을 알 수 있다.
좀더 실용적으로 접근 해서 내가 작업중인 함수단위 테스트를 추가 해보자.
/test/mytest.spec.js
파일을 생성 하고 아래 코드를 써준다.
// Jest test code
function sum(x, y) {
return x + y;
}
test("sum() should success : 1 + 2 => 3", () => {
expect(sum(1, 2)).toBe(3);
});
test("sum() should success : 10 + 10 => 20", () => {
expect(sum(10, 10)).toBe(20);
});
test("sum() should success : 100 + 1000 => 1100", () => {
expect(sum(100, 1000)).toBe(1100);
});
그리고 yarn test
커맨드로 테스팅을 해보자.
기존 1개의 테스트에 더해서 총 4개의 테스트케이스가 pass 한 것을 알 수 있다.
이번에는 TDD방법론을 적용 해서 multiplication(a, b)
함수를 정의 하고 요구사항은 a, b 두수를 곱한 값을 리턴 해주어야 한다고 가정 해보자.
그럼 가장먼저 할일은 테스트케이스를 추가 하는 것이다.
18-20 라인에 3줄 추가 해주었다. 2곱하기 4면 8이 기대 된다는 뜻이다.
그리고 테스트를 돌려 보면 FAIL 이 나온다.
➜ myjest git:(master) ✗ yarn test
yarn run v1.22.10
$ jest
PASS test/NuxtLogo.spec.js
FAIL test/mytest.spec.js
● multiplication() should success : 2 + 4 => 8
ReferenceError: multiplication is not defined
17 |
18 | test("multiplication() should success : 2 + 4 => 8", () => {
> 19 | expect(multiplication(2, 4)).toBe(8);
| ^
20 | });
21 |
at Object.<anonymous> (test/mytest.spec.js:19:3)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 4 passed, 5 total
Snapshots: 0 total
Time: 2.517 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
multiplication is not defined
라고 레퍼런스 에러가 발생 한다.
당연하다, 아직 그런 함수는 존재 하지 않기 때문이다.
이제부터 개발자가 할일은 그런 함수를 존재하게 해서 (테스트를 수정하지 않고) PASS를 만들어 내는 것이다. 마치 퍼즐과 비슷하다고 할 수있다.
multiplication()
함수를 윗부분에 작성 해보자.
function multiplication(x, y) {
// TODO
}
그리고 다시 테스트를 돌려보면 테스트 FAIL의 이유가 바뀐 것을 알 수 있다.
FAIL test/mytest.spec.js
● multiplication() should success : 2 + 4 => 8
expect(received).toBe(expected) // Object.is equality
Expected: 8
Received: undefined
에러 메시지를 자세히 읽어보자. Jest의 진면목이 여기서 발휘 된다.
23라인의 함수의 기대출력값이 8
이어야 하는데 undefined
가 나왔다고 매우 상세하게도 알려준다. 테스트 블록의 제목으로 지정 했던 String도 같이 출력 되기 때문에 에러 발생 즉시 그 의미를 알아 챌 수 있다.
테스트가 실패한 이유는 당연하게도 아직 함수에서 return값을 지정 하지 않았기 때문인데, 코드리뷰 등으로 아무리 커버 한다고 해도 개발자가 실수로 놓치고 넘어가기 쉬운 이런 부분조차도 하나하나 다 잡아주니 전체 프로젝트는 잔실수보다 코어로직 자체에 더 집중 할 수 있게 되는 것이다.
해당 함수에 리턴값을 넣어주자. 전체 소스 코드는 아래와 같이 될 것이다.
// Jest test code
function sum(x, y) {
return x + y;
}
function multiplication(x, y) {
return x * y;
}
test("sum() should success : 1 + 2 => 3", () => {
expect(sum(1, 2)).toBe(3);
});
test("sum() should success : 10 + 10 => 20", () => {
expect(sum(10, 10)).toBe(20);
});
test("sum() should success : 100 + 1000 => 1100", () => {
expect(sum(100, 1000)).toBe(1100);
});
test("multiplication() should success : 2 + 4 => 8", () => {
expect(multiplication(2, 4)).toBe(8);
});
그리고 yarn test
실행시 5개의 테스팅이 성공 하는 것을 확인 할 수 있다. (올그린)
만약 jest의 테스트 코드를 특정 위치나 특정 파일로만 지정 하고 싶다면, jest.config.js 파일에 testMatch
를 지정 하면 된다. (아래)
testMatch: [
'<rootDir>/test/**/*.spec.js',
'<rootDir>/test/**/*.test.js',
],
여기서는 /test
폴더 안에 있는 확장자의 파일과
확장자의 파일만을 테스트 범위에 포함 시키게 지정 하였다.
**
을 붙인 이유는 하위폴더에 있는 파일까지 전부 검사 하기 위함이다.
package.json
파일의 scripts
항목에 정의된 test에 아래처럼 옵션을 추가 할 수도 있다. 예를 들어 --watchAll
옵션을 추가 한다면 테스트가 실시간으로 감시 되며 테스트파일을 저장 할 때마다 실시간으로 갱신 되어 편리하다. (yarn dev
의 메커니즘과 유사)
"test": "jest --watchAll"
종료 하고싶다면 q
를 누르면 된다.
이상으로 Nuxt에서 Jest를 다루는 기초적인 방법을 알아보았다. 다음 포스팅에서는 VueX(Store) 라던가 API, Mocking 등을 다루는 방법도 알아보자.