TOC
참고
본 내용은 단위 테스트의 기술을 읽고 정리한
내용입니다.
책의 내용과 함께 개인적인 의견과 생각, 학습을 담아 작성하였습니다.
비동기 데이터 가져오기
작업 기다리기
done 콜백의 역할
- jest에게 테스트의 (성공적) 종료를 알려주는 역할
- 콜백을 사용하는 케이스
test('async task test (using done)', (done) => {
setTimeout(() => {
expect(true).toBe(true);
done();
}, 3000);
});
jest는 done이 호출될 때까지 기다려 비동기 작업이 완료된 시점으로 인식
done을 사용하지 않는 경우
test('sync task test', () => {
const result = 1 + 1;
expect(result).toBe(2);
});
타임아웃 설정
test('async task test (timeout for 10 seconds)', (done) => {
jest.setTimeout(10000);
setTimeout(() => {
expect(true).toBe(true);
done();
}, 6000);
});
- 타임아웃 시간을 10초로 설정
- 6초 후에 done을 호출하여 테스트 성공
async/await 사용
콜백 쓰는 것보다 더 깔끔해진다.
// 콜백 사용
test('NETWORK REQUIRED (callback)', (done) => {
samples.funcAsyncAwait().then((result) => {
expect(result.success).toBe(true);
expect(result.status).toBe('ok');
done();
});
});
// async/await 사용
test('NETWORK REQUIRED (async/await)', async () => {
const result = await samples.funcAsyncAwait();
expect(result.success).toBe(true);
expect(result.status).toBe('ok');
});
통합 테스트의 어려움
[참고] 함수의 호출이 비동기적이기에, 저자는 이를 통합 테스트라고 부른다.
- 긴 실행 시간: 단위 테스트에 비해 느린 실행 속도
- 불안정성: 환경에 따라 달라질 수 있는 실행 시간과 실패/성공 결과
- 테스트와는 관계없는 코드나 환경 검증
- 파악하는 데 더 많은 시간 소요
- (부정적인) 테스트 상황 재현의 어려움
- 신뢰하기 어려운 결과 : 테스트 실패가 외부인지 내부인지 알기 어려움
코드를 단위 테스트에 적합하게 만들기
진입점 분리 패턴
프로덕션 코드에서 순수 로직 부분을 별도의 함수로 분리하여 그 함수를 테스트의 시작점으로 사용하는 패턴
콜백을 새로운 함수로 분리하여 순수 논리 작업 단위의 진입점으로 사용
어댑터 분리 패턴
본질적으로 비동기적인 요소를 분리하고 이를 추상화하여 동기적인 요소로 대체할 수 있게 하는 패턴
- 비동기 코드를 의존성처럼 여기는 전략
- 비동기 코드를 대체하고 싶은 코드 취급
어댑터의 유형은 다양
모듈형
: 전체 모듈이나 파일을 스텁으로 만들어 특정 함수를 대체함수형
: 시스템에 함수나 값을 주입하는 경우. 주입된 값을 테스트에서 스텁으로 대체 가능객체 지향형
: 프로덕션 코드에서 인터페이스를 사용하고, 테스트에서 해당 인터페이스를 구현한 스텁을 만드는 경우
타이머 다루기
몽키패칭으로 타이머를 스텁으로 만들기
- 몽키 패칭: 프로그램이 실행 중인 동안 시스템 소프트웨어를 로컬에서 확장하거나 수정하는 방법
const Samples = require('./timing-samples');
describe('monkey patching', () => {
let originalTimeout;
// 전역 객체에 원래 타이머 함수를 보관
beforeEach(() => (originalTimeout = setTimeout));
// 각 테스트 종료 시 몽키 패칭된 타이머 함수를 복원
afterEach(() => (setTimeout = originalTimeout));
test('calculate1', () => {
setTimeout = (callback, ms) => callback(); // 타이머 함수 몽키 패칭(즉시 실행)
Samples.calculate1(1, 2, (result) => {
expect(result).toBe(3);
});
});
});
- 단점 1. 보일러 플레이트 코드가 많이 필요
- 단점 2. 몽키 패칭된 함수를 되돌려야 하는데 자주 놓친다.
jest로 setTimeout 대체
jest는 타이머 함수를 처리하기 위한 기능을 제공한다.
jest.useFakeTimers()
: 다양한 타이머 함수를 스텁으로 대체jest.resetAllTimers()
: 모든 가짜 타이머를 진짜 타이머로 재설정jest.advanceTimersToNextTimer()
: 가짜 타이머를 작동시켜 콜백 실행
describe('caclulate1 - with jest', () => {
beforeEach(() => jest.useFakeTimers());
beforeEach(() => jest.clearAllTimers());
test('fake timeout with callback', () => {
Samples.caculate1(1, 2, (result) => {
expect(result).toBe(3);
});
jest.advanceTimersToNextTimer(); // 다음 예정된 타이머 실행
});
});
- 위는 모두 동기적으도 동작한다.
done()
함수를 호출할 필요가 없다. advanceTimersToNextTimer()
함수를 반드시 실행한다. 아니면 가짜 타이머가 실행되지 않는다.
일반적인 이벤트 처리하기
이벤트 이미터(Event Emitter)
- 액션이 완료되었음을 알리기 위해 메시지를 보내 이벤트를 발생시키는 객체
- 해당 이벤트를 구독(subscribe)하고 이벤트 발생을 확인
const EventEmitter = require('events');
class Adder extends EventEmitter {
constructor() {
super();
}
add(a, b) {
const result = a + b;
this.emit('added', result);
return result;
}
}
module.exports = Adder;
describe('events based module', () => {
describe('add', () => {
it('generates addition event when called', (done) => {
const adder = new Adder();
adder.on('added', (result) => {
expect(result).toBe(3);
done();
});
adder.add(1, 2);
});
});
});
done()
함수를 사용함으로써 이벤트 발생 여부 확인 가능expect(x).toBe(y)
를 추가하면- 이벤트 매개변수로 전달된 값 확인 가능
- 이벤트가 trigger되었는지도 확인 가능
DOM 테스트 라이브러리
- 화면상의 요소를 찾는 보일러 플레이트가 많았다.
- 라이브러리를 사용하면 쉽게 해결 가능
- 텍스트를 기반으로 탐색 쿼리 실행
const { screen, fireEvent } = require('@testing-library/dom');
const loadHtml = (fileRelativePath) => {
const filePath = path.join(__dirname, fileRelativePath);
const innerHTML = fs.readFileSync(filePath);
document.documentElement.innerHTML = innerHTML;
return document.documentElement;
};
const loadHtmlAndGetUIElements = () => {
const docElem = loadHtml('index.html');
const button = getByText(docElem, 'Click Me', { exact: true });
return { window, docElem, button };
}
describe('index helper', () => {
test("dom test lib button click triggers change in page", () => {
const { window, docElem, button } = loadHtmlAndGetUIElements();
// 이벤트 실행 간소화
fireEvent.load(window);
fireEvent.click(button);
expect(findByText(docElem, 'Clicked', { exact: false }))toBeTruthy();
})
});
- 페이지 요소를 찾기 위해 id/test-id를 별도로 지정하지 않아도 된다.
{ exact: false }
옵션 : 대소문자 구분 안 함, 문자열 시작/끝에 누락된 문자 허용
총평
비동기 코드가 의존성급이라니
처음에는 비동기 코드를 다루기 위해 진입점을 분리하기도 하고, 심지어 어댑터 방식으로 의존성처럼 취급하기도 하는 걸 보며 이렇게까지 해야 하나? 했는데, 비동기 함수에 대한 테스트로 인해 꽤나 애를 먹었던 기억이 있어서 이해가 됐습니다. 테스트에 맞춰 처리하는 것도 힘든데, 테스트를 짜는 건 얼마나 어려울까.
하지만 잘 와닿지 않는 걸
이번 장은 내용을 정리하기에 급급했습니다. 실제로 비동기 함수를 테스트할 때 참고하려는 용도로 기록을 남기고, 컨셉만 이해하는 방식으로 가볍게 읽었습니다.