01.Unit Test (단위 테스트)
Updated:
1.테스트 개요
Unit Test
-
단위(Unit) 테스트란 데이터(상태), 함수(메소드), 컴포넌트 등의 정의된 프로그램 최소 단위들이 독립적으로 정상 동작하는지 확인하는 방법.
-
즉 주관적으로 정의된 것에 대한 하나하나를 테스트 하는 개념임.
-
나중에 수정할 부분이 있으면 테스트 한 항목을 기반으로 더 안정적으로 수정을 할 수 있습니다.
-
작성한 로직이 코드위주로 테스트를 하기 때문에 가볍고, 더 빠르게 테스트를 진행 할 수 있습니다.
-
대표적인 Unit Test tool 로는 Jest 가 있습니다. (JavaScript test 용)
E2E Test
-
E2E (End to End) 테스트란 어플리케이션의 처음부터 끝까지의 실제 사용자의 관점에서 사용 흐름을 테스트 하는 방법입니다.
-
실제 브라우저 화면에서 만들어진 사이트를 직접 사용하면서 사용하는 테스트입니다.
-
대표적인 E2E test tool 로는 cypress 가 있습니다.
2.테스트 환경설정
-
예제 App test 파일로는 Jacob’s DevLog 의 portfolio 에 있는
Book-Search App
을 통해 진행 하겠습니다. (Vue.js 기반) -
Vue.js 에서 unit test 를 진행하기 위해선 아래와 같은 package 를 설치 해야 합니다.
-
jest
-
vue test-utils
-
vue-jest
-
babel-jest
-
-
package 설치
$ npm i -D jest @vue/test-utils@next vue-jest@next babel-jest
- root 경로에
jest.config.js
파일 생성 -> 테스트 할 환경설정 설정
// in jest.config.js
module.exports = {
// 파일 확장자를 지정하지 않은 경우, Jest 가 검색할 확장자 목록입니다.
// 일반적으로 많이 사용되는 모듈의 확장자를 지정합니다.
// e.g `import HellowWorld from '~/components/HelloWorld';`
moduleFileExtensions: [
'js',
'vue'
],
// `~` 와 같은 경로 별칭을 매핑합니다.
// `< rootDir>` 토큰을 사용해 루트 경로를 참조할 수 있습니다.
moduleNameMapper: {
'^~/(.*)$': '<rootDir>/src/$1' // regexp 로 ~/ 로 시작되는 모든 문자열은 root 경로의 src 폴더안의 모든 경로를 말함
},
// 일치하는 경로에서는 모듈을 가져오지 않습니다.
// `<rootDir>` 토큰을 사용해 루트 경로를 참조할 수 있습니다.
modulePathIgnorePatterns: [
'<rootDir>/node_modules',
'<rootDir>/dist'
],
// jsdom 환경에 대한 URL 을 설정합니다.
// https://github.com/facebook/jest/issues/6766
testURL: 'https://localhost',
// 정규식과 일치하는 파일의 변환 모듈을 지정합니다.
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\.js$': 'babel-jest'
}
}
- eslint 가 설치 되어 있으면 test 시 충돌 할 수 있기 때문에 사전에 정의된 전역 변수를 미리 설정 해주어야 합니다. root 경로에 있는
.eslintrc.js
에서..
// in .eslintrc.js
module.exports = {
// 사전에 정의된 전역 변수 설정
env: {
browser: true,
node: true,
jest: true
}
3.초기 테스트 실행
- 예를 들어 하나의 js 파일을 test 하기 위해선 데이터(함수) 등을 export 할 수 있는 통로를 만들어 줘야지 test 작동 파일로 데이터를 내보내야 합니다. 다음과 같이 export 를 해주어야 합니다.
// in example.js
export function double(num) {
return num * 2
}
-
즉, vue 나 js 파일안에 있는 파일의 데이터나, 함수는 export 를 하지 않으면 결국 테스트를 할 수 없습니다.
-
script 상에서 jest 를 사용할 수 있게
packge.json
파일에서 script 등록을 다음과 같이 해줍니다.
"scripts": {
"test:unit": "jest --watchAl" // watchAll flag 은 변경사항이 발생 될 경우 자동으로 감지해서 test 환경을 다시한번 동작 시키는 flag 입니다
}
- test 할 부분 작성
// in example.test.js
import { expect, test } from '@jest/globals'
import { double } from './example'
test('Sample test', () => {
expect(123).toBe(123)
})
- terminal 에서
$ npm run test:unit
을 실행하면 다음과 같음 같은 메세지가 출력됩니다.
- 만약 test 에서 fail 할 경우 다음과 같이 출력 됩니다.
// in example.test.js
import { expect, test } from '@jest/globals'
import { double } from './example'
test('Sample test', () => {// expect 는 실제 들어오는 값이고, tobe 는 함수 실행 시, 출력이 예상되는 값을 가리킴
expect(123).toBe('123') // 일부로 123을 str 로 변환해서 test 진행 -> fail 유도
})
-
received
는 받은 값을 의미하고,expected
는 기대되는 값을 가리키는데 두개의 값이 일치 하지 않기 때문에test
에서fail
이 나타나게 됨 -
마찬가지로 다른 test 를 실행합니다. 일단
tobe()
에 임의의 값을 입력하고,test()
를 실행하면fail
이 나오는데expected
값을received
값으로 바꿔서 넣어주면pass
가 됩니다
import { expect, test } from '@jest/globals'
import { double } from './example'
test('parameter is a number', () => {
expect(double(3)).toBe(7)
})
4.Jest Globals
test()
-
test 라는 전역 함수를 사용해서 각각의 용도로 구분해줌. 첫 번째 parameter 는 test 의 이름을 명시하고, 두번째 parameter로는 callBack 함수를 사용해서 실제 테스트를 진행 해주면 되는거임
-
예)
test('did not rain', () => { expect(inchesOfRain()).toBe(0); });
describe()
-
테스트 함수들을 묶어 내는 함수. 일종의 test() 를 group 화 해준다는 것
-
형식도 test() 와 마찬가지로 처음 parameter 는 이름, 두번째는 callback 함수를 사용함
-
describe() 를 사용하는 이유는, before(), after() 함수를 사용하기 위해서 사용합니다
beforeAll()
-
위와 마찬가지로 이름, callback 형태로 사용
-
describe() 안에 모든 test 가 진행되기 전에 한번만 실행 합니다
afterAll()
- 모든 test 가 진행되고 나서 다음에 시행될 것을 명시함
beforeEach()
- 각각의 test 가 실행되기 직전에 한번씩 동작할 수 있는 구조
afterEach()
- 각각의 test 가 끝나고 나서 한번씩 동작할 수 있는 구조
import { expect, test } from '@jest/globals'
import { double } from './example'
describe('group1', () => {
beforeAll(() => {
console.log('beforeAll')
})
afterAll(() => {
console.log('afterAll')
})
beforeEach(() => {
console.log('beforeEach')
})
afterEach(() => {
console.log('afterEach')
})
test('Sample test', () => {
console.log('sample test')
expect(123).toBe(123) // expect 는 실제 들어오는 값이고, tobe 는 함수 실행 시, 출력이 예상되는 값을 가리킴
})
test('parameter is a number', () => {
console.log('parameter is a number')
expect(double(3)).toBe(6)
expect(double(10)).toBe(20)
})
test('No Parameter', () => {
console.log('No parameter')
expect(double()).toBe(0)
})
})
5.Jest Marchers
expect(), .tobe()
- 실제 값 (expect) 와 기대되는 값 (tobe) 를 서로 비교해서 test 를 비교하는 것
const can = {
name: 'pamplemousse',
ounces: 12,
};
describe('the can', () => {
test('has 12 ounces', () => {
expect(can.ounces).toBe(12);
});
test('has a sophisticated name', () => {
expect(can.name).toBe('pamplemousse');
});
});
excpect(), tobe()
의 비교 되는 값은 원시형 데이터 (문자, 숫자 , boolean 데이터) 경우에 사용되며, 참조형 데이터 (배열, 객체) 등은toEqual()
로 해주어야 합니다.
toEqul()
const can1 = {
flavor: 'grapefruit',
ounces: 12,
};
const can2 = {
flavor: 'grapefruit',
ounces: 12,
};
describe('the La Croix cans on my desk', () => {
test('have all the same properties', () => {
expect(can1).toEqual(can2);
});
test('are not the exact same can', () => {
expect(can1).not.toBe(can2);
});
});
-
위의 코드와 같이 객체 데이터의 재귀를 통해서 먼저 두개의 속성의 name 이 같은지 비교 하고, 그 다음에 실제 value 부분이 같은지 비교해서 같을 경우 pass 를 시키는 것입니다
-
변수가 다르기 때문에 can1 과 can2 는 다른 객체 데이터 임 (다른 메모리에 저장 되있는것) 내부값이 같더라도 code 상에서
can1 === can2
을 하게 되면false
가 됨.toEqual()
을 사용해서 다른 메모리에 저장 되어 있지만 실제 안에 들어 있는 값만 비교해서 test 를 합니다 -
.not
을 사용해서 마치 ! 와 같이 부정 연산자를 사용해서 비교 test 할 수 있습니다. -
test 예시 (
toBe()
,toEqual()
)
const userA = {
name: 'Jacob',
age: 80
}
const userB = {
name: 'Emma',
age: 22
}
test('데이터가 일치해야 합니다', () => {
expect(userA.age).toBe(80)
expect(userA).toEqual({
name: 'Jacob',
age: 80
})
})
test('데이터가 일치하지 않아야 합니다', () => {
expect(userB.name).not.toBe('Jacob')
expect(userB).not.toEqual(userA)
})
6.비동기 테스트
-
특정한 코드에 비동기 코드를 작성할 경우, 실제 test 의 입장에서는 얼마나 기다려야 되는지 모르기 때문에, 별도로 기다리지 않고 바로 test를 진행 합니다. 그래서 마지막 부분에 비동기 함수를 호출을 시켜 줘야지 test 입장에서 비동기 함수의 작동이 끝날때 까지 기다 립니다.
-
먼저 통과하는 테스트 code 부터 작성하는 것이 아니라, 실패하는 test 부터 작성을 해서 실제 어떠한 값이 제공이 되었고, 어떠한 값이 기대가 되서 test 가 실패 했는지 항상 확인을 한 다음에 코드를 수정해서 test 를 pass 하게 끔 만들어야 합니다.
// in example.js
export function asyncFn() {
return new Promise(resolve => {
setTimeout(() => {
resolve('Done!')
}, 2000)
})
}
// in example.test.js
import { describe, expect, test } from '@jest/globals'
import { asyncFn } from './example'
describe('비동기 테스트', () => {
test('done', (done) => {
asyncFn().then(res => {
expect(res).toBe('Done!')
done()
})
})
test('then', () => {
asyncFn().then(res => {
expect(res).toBe('Done!')
})
})
test('resolves', () => expect(asyncFn()).resolves.toBe('Done!'))
// 주로 비동기 test 를 할때는 async awit 을 사용해서 직관적인 code 를 작성하는 게 좋음
test('async/await', async () => {
const res = await asyncFn()
expect(res).toBe('Done!')
})
})
- 비동기 함수에서 setTimeout() 을 6초로 설정하면 아래와 같이 fail 이 나오게 됩니다. 왜냐면 timeout 의 최대 시간이 기본값으로 5초 (5000ms) 로 설정 되어 있기 때문입니다.
- 만약 6초 후까지 기다린 후에 실행하게 하려면, test code 에서 3번째 parameter 로써 시간을 바꿔서 명시 하면 됩니다. (첫번째는 test 의 이름, 두번째는 실제 test 의 내용을 명시하는 콜백함수, 세번째는 기본값으로 5000 이라는 값이 생략 되어 있는 것임)
import { describe, expect, test } from '@jest/globals'
import { asyncFn } from './example'
describe('비동기 테스트', () => {
// 주로 비동기 test 를 할때는 async awit 을 사용해서 직관적인 code 를 작성하는 게 좋음
test('async/await', async () => {
const res = await asyncFn()
expect(res).toBe('Done!')
}, 7000)
})
7.모의 (Mock)함수
-
단위 test 라는 것은 최소한의 단위 (함수의 로직, 데이터) 를 test 하는 것이기 때문에 걸리는 시간도 최소한 시켜 줘야 합니다. 최대한 시간이 걸리는 logic 을 단순화 시켜 줄 필요가 있습니다. 그때, 사용되는것이 모의 함수 라는 것입니다.
-
테스트에 방해되는 개념 (지연시간 등) 을 무시하고, 가짜(모의)의 개념으로 함수를 만들어서 value 값만 확인할 수 있게 test 를 진행 할 수 있습니다.
// 예시로 이전의 code 에서 지연시간이 7000으로 설정 되어 있지만, 모의 함수를 만들어서 지연시간을 무시하고 빠른 시간에 test를 진행할 수 있음
import { describe, expect, jest, test } from '@jest/globals'
import * as example from './example'
describe('비동기 테스트', () => {
test('async/await', async () => {
jest.spyOn(example, 'asyncFn').mockResolvedValue('Done!') // 모의 함수 실행
const res = await example.asyncFn()
expect(res).toBe('Done!')
}, 7000)
})
-
비동기 테스트를 진행 할 경우 외부요인들의 영향을 받지 않고 test 가 pass 될 수 있게 모의 함수를 잘 사용해야 합니다. (예 인터넷 연결이 안될 경우 외부 API 호출이 안되서 test 가 실패하지만, 모의 함수를 만들어 놓고 test 를 진행하면 외부 요인에 영향을 받지 않습니다.)
-
예시로 ombd api 에서 title Frozen II 를 Frozen ii 로 변환 하는 test 를 모의함수를 통해서 test 를 진행하는 code 예시 입니다.
// in example.js
import axios from 'axios';
import _upperFirst from 'lodash/upperFirst'
import _toLower from 'lodash/toLower'
export async function fetchMovieTitle() {
const res = await axios.get('https://omdbapi.com?apikey=7035c60c&i=tt4520988')
return _upperFirst(_toLower(res.data.Title)) // Frozen II => Frozen ii 영화 제목 뒤에 대문자 I 를 소문자 i 로 바꿔서 출력하기
}
// in example.test.js
import { describe, expect, jest, test } from '@jest/globals';
import { fetchMovieTitle } from './example';
import axios from 'axios'
describe('비동기 테스트', () => {
// omdb 에서 호출하는 결과 기대 값을 임의로 모의 함수를 만들어서 가지고 오는 db runtime 에 상과 없이 테스트를 진행 할 수 있음
// 서버에서 데이터를 가지고 올 수 없어도 test 를 할 수 있는거임 (즉, 인터넷이 연결이 안된 상태에서도 test 는 된다는 의미임)
axios.get = jest.fn(() => {
return new Promise(resolve => {
resolve({
data: {
Title: 'Frozen II'
}
})
})
})
test('영화제목 변환', async () => {
const title = await fetchMovieTitle()
expect(title).toBe('Frozen ii')
})
})
🔶 🔷 📌 🔑
Reference
-
jest docs - https://jestjs.io/docs/api
-
A Beginner’s Guide to Unit-testing with Jest - https://dev.to/dsasse07/a-beginner-s-guide-to-unit-testing-with-jest-45cc
Leave a comment