TOC
참고
본 내용은 단위 테스트의 기술을 읽고 정리한 내용입니다.
책의 내용과 함께 개인적인 의견과 생각, 학습을 담아 작성하였습니다.
Jest
Jest에 대하여
__tests__
폴더 내의 모든 파일*.spec.js
또는*.test.js
파일- test와 spec은 같은 의미이므로 취향껏 선택
- 다만 test보다 spec이 규모적으로 큰 테스트를 의미하는 경우가 많음
jest는 자동으로 global 함수를 불러옴(import)
- 테스트 대상 파일이나 모듈 옆에 둠
- 테스트 대상 파일이나 모듈을 찾기 쉬움
- 모든 테스트 파일을 별도 폴더 아래에 둠
- 테스트에 필요한 헬퍼 파일을 테스트 폴더 근처에 둘 수 있어서 편함
Jest의 실행
jest
명령어를 사용하여 테스트 실행jest --watch
명령어를 사용하여 테스트 실행 후 파일 변경 감지jest --coverage
명령어를 사용하여 테스트 커버리지 확인
테스트 Skill
USE 전략
-
Unit: 테스트하려는 대상
-
Senario: 입력 값이나 상황에 대한 설명
-
Expectation: 기댓값이나 결과에 대한 설명
-
1줄 요약: "테스트 대상을 명시하고, 어떤 입력이나 상황이 주어지면 어떤 결과로 이어져야 하는지 간결하고 명확하게 적는다."
-
이유: 보통 테스트 이름과 결과만 터미널에 간략히 표시하므로
문자열 비교
- 문자열은 화면에 보이고, 비즈니스 기획에 따라 일부 수정이 되어야 할 수도 있다.
- 하지만 중요한 건 메시지가 담고 있는 의미
- 텍스트 비교는 동일한 비즈니스 로직에서는 항상 같은 결과를 보장해야 한다.
toMatch
를 쓸 거라면 정규 표현식을 사용한다.toContain
은 문자열 중간에 포함된 문자열을 찾을 수 있어 핵심 용어를 넣어 비교
구역의 분리
describe
함수로 구역을 나눠라.describe
함수는 테스트 그룹을 만들 수 있다.- reporting(terminal)에서도 더 구조적으로 구분감 있게 나온다.
describe('테스트 대상', () => {
test('어떤 입력이나 상황, 어떤 결과', () => {
expect(테스트 대상).toBe(기댓값);
});
});
그리고 이렇게 중첩 사용도 가능
describe('테스트 대상', () => {
describe('어떤 입력이나 상황', () => {
test('어떤 결과', () => {
expect(테스트 대상).toBe(기댓값);
});
});
});
이를 통해 위의 USE 전략을 구조적으로 분리할 수 있다.
단, 어떤 게 더 우월하다기보다는 nested할 때와 linear할 때 더 좋은 상황을 잘 구분하여 적절히 사용한다. 중간점을 잘 찾아야 한다.
의미론적 연결
it
함수를 사용하라.it
함수는test
함수의 별칭- 둘 다 같은 기능을 수행하지만 의미론적으로 더 명료하다.
- describe it ~~
- it returns errors
검증 룰렛 피하기
- 하나의 테스트 케이스에 여러 검증을 넣은 상황
- 어떤 검증이 유효한지 확인하기 위해 앞선 검증 코드들을 주석 처리
- 이는 많은 혼란과 잘못된 거짓 양성을 초래
verifier.addRule(fakeRule);
const errors = verifier.verify('any value');
expect(errors).toHaveLength(1);
expect(errors[0]).toContain('fake reason');
차라리 it문
을 여러 개 쓰더라도 별도의 테스트 케이스로 분리하기
it('테스트 대상', () => {
verifier.addRule(fakeRule);
const errors = verifier.verify('any value');
expect(errors).toHaveLength(1);
});
it('테스트 대상', () => {
verifier.addRule(fakeRule);
const errors = verifier.verify('any value');
expect(errors[0]).toContain('fake reason');
});
그런데... 중복 코드가 좀 많다?
beforeEach 함수
중복을 제거하는 beforeEach 함수
describe('테스트 대상', () => {
let verifier;
beforeEach(() => {
verifier = new Verifier();
});
describe('어떤 입력이나 상황', () => {
let fakeRule, errors;
beforeEach(() => {
fakeRule = new FakeRule();
errors = verifier.verify('any value');
});
it('어떤 결과1', () => {
expect(errors).toHaveLength(1);
});
it('어떤 결과2', () => {
expect(errors[0]).toContain('fake reason');
});
});
});
beforeEach 함수의 주의사항
공유 상태에 주의하라
- Jest는 단위 테스트를 병렬로 실행
- 따라서 공유되는 상태를 만들면 테스트 간에 영향을 줄 수 있다.
스크롤 피로감에 주의하라
- 중복 코드를 없애기 위해 상위 scope로 중복코드를 이동
- 때문에 말단인
it문
에서는 전체 흐름을 알기 어려움(코드의 분산)
팩토리 함수
팩토리 함수의 사용
beforeEach
함수를 사용하여 발생한 스크롤 피로감과 코드의 분산의 해결책- 복잡한 코드의 추상화, 그리고 이의 추상화
// 원소 단위
const makeVerifier = () => new PasswordVerifier();
const passingRule = () => ({ passed: true, reason: '' });
const failingRule = () => ({ passed: false, reason: 'fake reason' });
// 분자 단위
const makeVerifierWithPassingRule = () => {
const verifier = makeVerifier();
verifier.addRule(passingRule());
return verifier;
};
const makeVerifierWithFailingRule = () => {
const verifier = makeVerifier();
verifier.addRule(failingRule());
return verifier;
};
// 조직 단위
describe('v8 PasswordVerifier', () => {
describe('성공하는 상황', () => {
it ('결과1', () => {
const verifier = makeVerifierWithPassingRule();
expect(...);
});
it ('결과2', () => {
const verifier = makeVerifierWithPassingRule();
expect(...);
})
});
describe('실패하는 상황', () => {
it ('결과1', () => {
const verifier = makeVerifierWithFailingRule();
expect(...);
});
it ('결과2', () => {
const verifier = makeVerifierWithFailingRule();
expect(...);
});
});
});
팩토리 함수의 장점
- beforeEach 함수를 사용한 듯한 유지보수성
- 코드의 분산 감소(
it문
에서 모든 정보 파악 가능) - 스크롤 피로감 감소
다양한 입력값을 받는 테스트 리팩터링
test.each(it.each)
다양한 입력값을 받는 테스트를 쉽게 작성할 수 있게 해준다.
describe('테스트_대상', () => {
it('결과1', () => {
const result = 테스트_대상(input1);
expect(result).toBe(expected1);
});
it('결과2', () => {
const result = 테스트_대상(input2);
expect(result).toBe(expected2);
});
...
});
이런 반복 코드를 아래와 같이 함축한다.
describe('테스트_대상', () => {
test.each([
[input1, expected1],
[input2, expected2],
...
])('input: %s, expected: %s', (input, expected) => {
expect(테스트_대상(input)).toBe(expected);
});
});
단, 이 방식은 복잡해질 경우 오히려 가독성을 해칠 수 있으니 주의가 필요
테스트 카테고리 설정
--testPathPattern 사용
jest가 테스트를 찾는 방식을 정의(참고: jest 공식 문서)
별도의 jest.config.js 파일
- 각 테스트 카테고리에 대해 별도의 jest.config.js 파일 생성
- 각 파일에
testRegex
옵션과 다른 설정을 지정
// jest.config.integration.js
var config = require('./jest.config');
config.testRegex = 'integration/.*\\.js$';
module.exports = config;
// jest.config.unit.js
var config = require('./jest.config');
config.testRegex = 'unit/.*\\.js$';
module.exports = config;
이렇게 하면 각 테스트 카테고리에 대해 별도의 npm 스크립트를 만들 수 있다.
// package.json
{
"scripts": {
"test:unit": "jest -c jest.config.unit.js",
"test:integration": "jest -c jest.config.integration.js"
}
}
총평
jest를 맛보다
그동안 토스 채용 프로세스나 우아한 테크캠프 등 단위 테스트 라이브러리로 jest를 '소비'해오기만 했어요. 그런데 이제는 생산자로서 테스트를 실제로 짜고 이를 활용할 수 있게 된 것이 뿌듯하고 좋은 시작이 되었어요. 그렇게 어렵지도 않은데 그동안 우선순위에 밀려서 개인 개발에서도, 회사 개발에서도 포기했는데, 이렇게라도 접해보니 즐겁습니다. 좋은 구조들과 방법론 좋은 코드들을 고민해볼 수 있는 시작이 되길 바라요.
중용이 어렵다
describe를 사용하여 구조적으로 계층을 나눈 nested한 구조, 그리고 test를 사용해 linear하게 나누는 것을 비교하는 챕터에서 결국 결론은, '중간을 잘 찾고 지킨다'였어요. 그런데 이게 참 어렵다고 느껴집니다. 여기부터는 사람들 사이의 생각과 선호가 있으니까요. 많이 짜보고 경험을 쌓으면 어떤 상황에 어떻게 하는 게 유리할 지 잘 선택할 수 있게 되겠죠.
왜 테스트 환경이 안 되지
jest 실행이 책에서 하라는대로 따라했는데도 잘 되지 않아 많이 헤맸어요. 사실 지금은 독서 스터디 프로젝트에 미리 세팅된 vitest 설정에 따라 실습을 해보고 있어 당장은 필요 없지만 왜 그런지 이유를 찾아볼 필요가 있겠습니다.