최근 발행-구독 패턴을 활용하기 위한 메시지 브로커를 구현하면서 주어진 시간 간격으로 사용하지 않는 채널을 정리하는 기능을 추가하였는데요
이를 위해 setInterval
을 활용하여 특정 기능을 설정한 주기로 실행시키는 Scheduler
를 구현하였습니다.
구현한 Scheduler
의 테스트코드를 작성하며 Jest
의 FakeTimer
를 활용하였는데 이 경험을 공유해보려해요
먼저 Scheduler
를 살펴볼까요?
Scheduler
Scheduler
는 아래와 같은 기능을 가지고 있습니다.
- 작업 등록/해제
registerTask
: 새로운 주기적 작업 등록unregisterTask
: 등록된 작업 제거
- 작업 실행 제어
start
/stop
: 개별 작업의 시작과 중지startAll
/stopAll
: 모든 작업의 일괄 시작과 중지
- 동시성 제어
runningTasks
를 통한 동일 작업의 중복 실행 방지
실제 Scheduler
구현에서 setInterval
의 동작을 확인해야하는 코드들만 추려봤습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| export interface ScheduledTask {
readonly name: string;
readonly interval: number;
execute(): Promise<void>;
}
export class Scheduler {
private readonly timers: Map<string, NodeJS.Timeout> = new Map();
private readonly runningTasks: Set<string> = new Set();
constructor(private tasks: ScheduledTask[] = []) {
}
public registerTask(task: ScheduledTask) {
if (this.tasks.find((t) => t.name === task.name)) {
throw new Error(`Task with name ${task.name} already exists`);
}
this.tasks.push(task);
}
public start(taskName: string) {
const task = this.tasks.find((task) => task.name === taskName);
if (!task) {
throw new Error(`Task ${taskName} not found`);
}
if (this.timers.has(taskName)) {
return;
}
const timer = setInterval(() => this.executeTask(task), task.interval);
this.timers.set(taskName, timer);
}
private async executeTask(task: ScheduledTask) {
if (this.runningTasks.has(task.name)) {
return;
}
this.runningTasks.add(task.name);
try {
await task.execute();
} finally {
this.runningTasks.delete(task.name);
}
}
}
|
ScheduledTask
의 구현체들을 받아, 설정한 주기대로 setInterval
을 적용해주는 간단한 처리입니다.
타이머 API를 이용한 테스트
먼저 FakeTimers
를 사용하지 않고 start
를 수행하였을 때의 테스트 코드를 살펴볼까요?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| describe('Scheduler (without FakeTimers)', () => {
let tasks: ScheduledTask[];
let scheduler: Scheduler;
beforeEach(() => {
tasks = [];
scheduler = new Scheduler(tasks);
});
describe('start', () => {
it('지정된 간격으로 작업이 실행되어야 한다', (done) => {
const mockTask = createMockTask('test-task', 1000);
const tasks = [mockTask];
const scheduler = new Scheduler(tasks);
scheduler.start(mockTask.name);
setTimeout(() => {
expect(mockTask.execute).toHaveBeenCalledTimes(3);
scheduler.stop(mockTask.name);
done();
}, 3000);
});
});
it('동일한 작업이 동시에 실행되지 않아야 한다', (done) => {
let executionCount = 0;
const mockTask = createMockTask('test-task', 1000);
mockTask.execute.mockImplementation(async () => {
executionCount++;
await new Promise((resolve) => setTimeout(resolve, 2000));
});
const tasks = [mockTask];
const scheduler = new Scheduler(tasks);
scheduler.start(mockTask.name);
setTimeout(() => {
expect(executionCount).toEqual(1);
setTimeout(() => {
expect(executionCount).toEqual(2);
scheduler.stop(mockTask.name);
done();
}, 1500);
}, 1500);
});
});
|
위 테스트 코드는 mockTask
의 execute
에 담겨있는 jest.fn().mockResolvedValue(undefined)
를 1초마다 한번씩 실행시켜서 그 결과를 확인하는 테스트와 스케줄러를 통해
실행되는 작업이 아직 완료되지 않았을 경우 해당 작업이 실행되지 않는 것을 확인하는 테스트입니다.
테스트 코드를 이해하기위해 먼저 타이머 API의 동작을 간단히 설명이 필요할 것 같은데요
싱글스레드로 동작하는 자바스크립트는 하나의 콜스택만 사용할 수 있습니다. 따라서 setTimeout
또는 setInterval
과 같은 타이머 API를 통해 넘겨진 동작(콜백 함수)는 별도의 공간에서
카운트다운을 시작하게됩니다.
카운트다운이 완료되면 해당 콜백 함수가 태스크 큐(매크로태스크 큐)에 들어가게되고, 이벤트 루프가 콜스택이 비었을 때 태스크 큐에 담겨있는 콜백을 실행하게 됩니다.
그렇기 때문에 Scheduler.start
메서드가 실행되면 setInterval
의 콜백으로 ScheduledTask.excute
를 넘겨주게되고, 카운트다운이 완료되면 태스크 큐로 들어간 후 자신의 차례가
되었을 때 실행되게 됩니다.
그렇기 때문에 setTimeout
의 콜백으로 expect
를 넘겨줘야 Scheduler.start
가 실행되고 검증 처리를 수행할 수 있게 됩니다.
문제점
예시처럼 타이머 API를 직접 활용하는 테스트 코드에는 몇 가지 문제점이 있습니다.
실제 시간에 의존
첫 번째로 테스트가 실제 시간에 의존한다는 것 인데요
실제 시간을 활용하는 타이머 API를 사용하기 때문에, 테스트의 interval
로 주어진 시간만큼 필요하게 됩니다.
따라서 interval
값을 크게 줄 경우 그 시간만큼 테스트가 길어지게 되는 것이죠
이 때문에 좋은 테스트 코드의 조건인 빠른 시간에 적합하지 않을 수 있고, 이를 해결하기 위해 interval
을 작은 값으로 지정하면 또 다른 문제가 발생할 수 있습니다.
신뢰성
두 번째로 신뢰성 인데요. 테스트의 신뢰성은 동일한 조건에서는 항상 동일한 결과가 나와야 한다는 의미인데, 타이머 API의 특성으로 인해 동일한 결과가 나오지 않을 수 있습니다.
앞서 설명드렸던 것 처럼 타이머 API를 통해 넘겨진 콜백은 카운트다운이 완료된 후 태스크 큐에 적재되고, 먼저 적재된 작업들을 메인 스레드가 다 처리해야 해당 작업을 메인 스레드가 처리하게 되는데요
이로 인해 타이머 API가 설정한 시간에 도달해서 태스크 큐에 작업을 적재하더라도 즉시 실행되는 것은 보장할 수 없습니다.
- 우선 순위가 더 높은 작업들이 큐에 많이 적재되어 있는 경우
- 선행 작업이 메인 스레드를 오래 점유하는 경우
그렇기 때문에 시스템 부하에 따라 호출 횟수가 달라져 테스트를 통과하지 못할 여지가 있습니다.
물론 현재 테스트코드는 가볍고, 양이 적기 때문에 당장 문제가 없을 지 모르지만 아예 배제할수는 없는 것이죠
FakeTimers란?
Jest의 FakeTimers는 테스트 환경에서 시간 관련 함수들을 모킹하여 시간의 흐름을 제어할 수 있게 해주는 기능입니다. 내부적으로는 @sinonjs/fake-timers
를 사용하여 구현되어 있습니다.
FakeTimers를 사용하면 setTimeout
, setInterval
, clearTimeout
, clearInterval
등의 타이머 API를 가짜 구현체로 대체하여 실제 시간이 흐르는 것을
기다리지 않고도 타이머 기반 코드를 테스트할 수 있습니다.
주요 기능 분석
FakeTimers는 크게 세 가지 중요한 기능을 제공합니다.
타이머 제어
가짜 타이머를 활성화하고 관리하는 기능들입니다.
1
2
3
4
5
6
7
8
| // 가짜 타이머 활성화
jest.useFakeTimers();
// 실제 타이머로 복원
jest.useRealTimers();
// 모든 타이머 초기화
jest.clearAllTimers();
|
useFakeTimers()
:
- 모든 타이머 관련 함수(setTimeout, setInterval 등)를 가짜 구현체로 교체
- 테스트 환경에서 시간을 완벽하게 제어할 수 있게 됨
- 설정 옵션을 통해 특정 타이머만 가짜로 교체하는 것도 가능
useRealTimers()
:
- 모든 타이머 함수를 원래의 구현체로 복원
- 테스트 종료 후 정리 작업에 필수
clearAllTimers()
:
- 현재 대기 중인 모든 타이머를 제거
- 타이머 관련 상태를 완전히 초기화할 때 사용
시간 진행 제어
시간의 흐름을 제어하여 타이머의 실행을 관리하는 기능들입니다.
1
2
3
4
5
6
7
8
| // 대기 중인 모든 타이머 즉시 실행
jest.runAllTimers();
// 현재 대기 중인 타이머만 실행
jest.runOnlyPendingTimers();
// 특정 시간만큼 진행
jest.advanceTimersByTime(1000); // 1초 진행
|
runAllTimers()
:
- 현재 예약된 모든 타이머를 즉시 실행
- 재귀적인 타이머(타이머 내에서 새로운 타이머를 생성하는 경우)도 모두 실행
- 무한 루프 방지를 위해 기본적으로 100,000회로 제한됨
runOnlyPendingTimers()
:
- 현재 대기 중인 타이머만 실행하고 새로 생성되는 타이머는 실행하지 않음
- 재귀적인 타이머를 한 단계씩 테스트할 때 유용
1
2
3
4
5
6
7
8
9
10
| // 예: 재귀적 타이머 테스트
function recursiveTimer() {
setTimeout(() => {
console.log('tick');
recursiveTimer();
}, 1000);
}
recursiveTimer();
jest.runOnlyPendingTimers(); // 첫 번째 타이머만 실행
|
advanceTimersByTime()
:
- 가상의 시간을 특정 밀리초만큼 진행
- 진행되는 시간 동안 실행되어야 할 모든 타이머를 실행
- 시간의 흐름을 가장 정확하게 시뮬레이션할 수 있는 방법
비동기 작업 지원
Promise 기반의 비동기 작업을 지원하는 기능들입니다.
1
2
3
| // Promise와 함께 사용할 수 있는 비동기 버전
await jest.runAllTimersAsync();
await jest.advanceTimersByTimeAsync(1000);
|
FakeTimers를 이용한 테스트
이제 앞서 보았던 테스트 코드를 FakeTimers 적용해보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| describe('Scheduler', () => {
let tasks: ScheduledTask[];
let scheduler: Scheduler;
beforeEach(() => {
tasks = [];
scheduler = new Scheduler(tasks);
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
jest.clearAllMocks();
});
describe('start', () => {
it('지정된 간격으로 작업이 실행되어야 한다', async () => {
const mockTask = createMockTask('test-task', 1000);
const tasks = [mockTask];
const scheduler = new Scheduler(tasks);
scheduler.start(mockTask.name);
await jest.advanceTimersByTimeAsync(3000);
expect(mockTask.execute).toHaveBeenCalledTimes(3);
});
});
describe('동시 실행 방지', () => {
it('동일한 작업이 동시에 실행되지 않아야 한다', async () => {
const mockTask = createMockTask('test-task', 1000);
const tasks = [mockTask];
const scheduler = new Scheduler(tasks);
// 2초가 걸리는 작업
mockTask.execute.mockImplementation(() =>
jest.advanceTimersByTimeAsync(2000),
);
scheduler.start(mockTask.name);
await jest.advanceTimersByTimeAsync(1500);
expect(mockTask.execute).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(1500);
expect(mockTask.execute).toHaveBeenCalledTimes(2);
});
});
});
|
뭔가 더 깔끔해지지 않았나요?! FakeTimers를 적용하여 개선된 부분을 살펴보면 아래와 같습니다.
실행 속도

위 테스트 결과는 작성된 모든 테스트 코드를 통해 실행 속도를 측정한 결과인데요, 보시는 것 같이 FakeTimers을 적용하지 않았을 때 21초 이상, FakeTimers를 적용한 후에 25ms 소요되었습니다.
앞서 언급했던 것 처럼 FakeTimers를 활용하기 때문에 실제 시간을 기다리지 않고 즉시 실행되어 실제 타이머의 동작을 기다리는 이전 방식과는 많은 차이가 발생할 수 밖에 없습니다.
정확성
FakeTimers를 사용했을 때 정확성 측면에서 아래와 같은 장점들을 취할 수 있습니다.
- 시스템 부하와 관계없이 일관된 결과를 보장
- 실제 타이머는 시스템 상태에 따라 지연될 수 있지만, FakeTimers는 정확한 시점에 실행

FakeTimers 사용 전에는 마지막 테스트인 동일한 작업이 동시에 실행되지 않아야 한다가 실패하는 것을 확인하실 수 있는데요

하지만 이 테스트는 독립적으로 실행되었을 때는 통과되는 문제(?)가 있었습니다. (실패하는 경우도 있었어요😂)
타이머를 실행했을 때 마다 결과가 달라지는 원인으로 가장 흔한 경우는 실행이 지연되는 경우이죠
이처럼 FakeTimers를 활용하면 일관된 결과를 보장할 수 있습니다.
제어 용이성
또 다른 장점으로 시간의 흐름을 정확하게 제어할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| it('동일한 작업이 동시에 실행되지 않아야 한다(ㅈwithout FakeTimers', (done) => {
let executionCount = 0;
const mockTask = createMockTask('test-task', 1000);
mockTask.execute.mockImplementation(async () => {
executionCount++;
await new Promise((resolve) => setTimeout(resolve, 2000));
});
const tasks = [mockTask];
const scheduler = new Scheduler(tasks);
scheduler.start(mockTask.name);
setTimeout(() => {
expect(executionCount).toEqual(1);
setTimeout(() => {
expect(executionCount).toEqual(2);
scheduler.stop(mockTask.name);
done();
}, 1500);
}, 1500);
});
it('동일한 작업이 동시에 실행되지 않아야 한다', async () => {
const mockTask = createMockTask('test-task', 1000);
const tasks = [mockTask];
const scheduler = new Scheduler(tasks);
// 2초가 걸리는 작업
mockTask.execute.mockImplementation(() =>
jest.advanceTimersByTimeAsync(2000),
);
scheduler.start(mockTask.name);
await jest.advanceTimersByTimeAsync(1500);
expect(mockTask.execute).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(1500);
expect(mockTask.execute).toHaveBeenCalledTimes(2);
});
|
advanceTimersByTime
혹은 advanceTimersByTimeAsync
를 통해 원하는 시점으로 정확하게 이동할 수 있습니다.
위처럼 비교적 복잡한 시나리오(예: 타이머 중첩, 긴 실행 시간)도 쉽게 테스트할 수 있습니다. 가독성도 높아집니다!
주의사항
제가 FakeTimers를 사용하면서 겪었던 문제들을 통해 주의해야하는 내용들도 공유드려볼까합니다.
비동기 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| it('비동기 작업 테스트 시 주의사항', async () => {
const mockTask = createMockTask('test-task', 1000);
const tasks = [mockTask];
const scheduler = new Scheduler(tasks);
mockTask.execute.mockImplementation(async () => await someAsyncOperation());
scheduler.start(asyncTask.name);
// 잘못된 방법
jest.advanceTimersByTime(1000);
await Promise.resolve(); // 마이크로태스크 큐 처리
expect(asyncTask.execute).toHaveBeenCalled(); // 실패할 수 있음
// 올바른 방법
await jest.advanceTimersByTimeAsync(1000);
expect(asyncTask.execute).toHaveBeenCalled();
});
|
advanceTimersByTime
는 원하는 시점으로 시간을 정확히 이동시키기는 하지만, async
처럼 Promise를 반환하는 함수를 사용할 때 주의가 필요합니다.
이 부분은 자바스크립트의 이벤트 루프와 Promise의 동작 방식과 관련이 있는데요, 이벤트 루프는 매크로태스크 큐, 마이크로태스크 큐 순으로 작업들을 처리하기 때문입니다.
jest.advanceTimersByTime()
은 매크로태스크 큐에 있는 타이머 콜백들을 실행시키지만, Promise로 만들어진 마이크로태스크들은 실행시키지 않습니다.
위 예시에서 someAsyncOperation()
는 Promise를 반환하는 비동기 함수로, 이 Promise의 처리(then
/catch
등)는 마이크로태스크 큐에 들어가게 됩니다.
실행 순서를 자세히 보면:
jest.advanceTimersByTime(1000)
setInterval
의 콜백을 실행 (매크로태스크)executeTask
함수가 호출됨someAsyncOperation()
가 Promise를 반환
- 이 시점에서는:
someAsyncOperation()
의 결과로 생성된 Promise의 처리가 마이크로태스크 큐에 있음
await Promise.resolve()
- 마이크로태스크 큐를 비움
- Promise의 처리가 실행됨
- finally 블록이 실행됨
따라서 await Promise.resolve()
가 없다면 Promise 처리가 완료되지 않은 상태에서 다음 테스트 코드가 실행되는 등 결과적으로 테스트가 실패하거나 예상치 못한 동작을 할 수 있습니다.
때문에 비동기 로직의 경우 advanceTimersByTimeAsync
를 사용하는 것을 추천드리겠습니다.
클린업
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| describe('Timer 테스트', () => {
// 잘못된 방법
it('첫 번째 테스트', () => {
jest.useFakeTimers();
// ... 테스트 코드
});
it('두 번째 테스트', () => {
// 이전 테스트의 타이머 상태가 영향을 미칠 수 있음
// ... 테스트 코드
});
// 올바른 방법
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
jest.clearAllMocks();
});
});
|
각 테스트가 끝난 후 jest.useRealTimers()
를 호출하여 타이머를 원래 상태로 복원해야 합니다.
타이머 상태 누수 문제
타이머 상태가 테스트 간에 누수되면 예상치 못한 동작이 발생할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| describe('타이머 누수 예시', () => {
// 잘못된 방식
it('첫 번째 테스트', () => {
jest.useFakeTimers();
const scheduler = new Scheduler();
const task = createMockTask('task', 1000);
scheduler.start(task.name);
jest.advanceTimersByTime(1000);
// 타이머가 정리되지 않은 상태로 테스트 종료
});
it('두 번째 테스트', () => {
jest.useFakeTimers();
// 이전 테스트의 타이머가 여전히 활성화 상태일 수 있음
// 예상치 못한 동작 발생 가능
});
});
// 올바른 방식
describe('타이머 정리 예시', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
jest.clearAllTimers();
jest.clearAllMocks();
});
it('안전한 테스트', () => {
const scheduler = new Scheduler();
const task = createMockTask('task', 1000);
scheduler.start(task.name);
jest.advanceTimersByTime(1000);
});
});
|
비동기 작업과 타이머 정리
비동기 작업이 완료되기 전에 타이머가 정리되면 테스트가 실패할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| describe('비동기 작업과 타이머', () => {
// 문제가 될 수 있는 상황
it('비동기 작업 중 타이머 정리', async () => {
jest.useFakeTimers();
const scheduler = new Scheduler();
const task = {
name: 'async-task',
interval: 1000,
execute: jest.fn().mockImplementation(async () => {
await someAsyncOperation();
// 이 시점에서 타이머가 이미 정리되었다면?
await somethingElse();
})
};
scheduler.start(task.name);
jest.advanceTimersByTime(1000);
jest.useRealTimers(); // 너무 일찍 타이머 정리
});
// 올바른 방식
it('안전한 비동기 작업 테스트', async () => {
jest.useFakeTimers();
const scheduler = new Scheduler();
const task = {
name: 'async-task',
interval: 1000,
execute: jest.fn().mockImplementation(async () => {
await someAsyncOperation();
await somethingElse();
})
};
scheduler.start(task.name);
await jest.advanceTimersByTimeAsync(1000);
jest.useRealTimers();
});
});
|
실제 시간과의 차이
1
2
3
4
5
6
7
8
9
10
11
| it('실제 시간 동작과 차이가 있을 수 있음', () => {
const longRunningOperation = () => {
// 실제 환경: CPU 부하에 따라 지연될 수 있음
// FakeTimers: 정확히 1초 후 실행
setTimeout(() => heavyComputation(), 1000);
};
longRunningOperation();
jest.advanceTimersByTime(1000);
// 실제 환경과 다른 결과가 나올 수 있음
});
|
FakeTimers는 가상의 시간을 사용하므로, 실제 환경과 약간의 차이가 있을 수 있습니다.
CPU 부하, 시스템 상태 등에 따른 지연을 시뮬레이션할 수 없기 때문에 중요한 시간 관련 로직은 실제 환경에서도 검증이 필요할 수 있습니다.
CPU 부하에 따른 타이밍 차이
실제 환경에서는 CPU 부하에 따라 타이머 실행이 지연될 수 있지만, FakeTimers는 이를 시뮬레이션하지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| describe('CPU 부하와 타이밍', () => {
it('FakeTimers vs 실제 환경의 차이', async () => {
const task = createMockTask('task', 1000);
const tasks = [task];
const scheduler = new Scheduler(tasks);
mockTask.execute.mockImplementation(async () => {
for (let i = 0; i < 1000000000; i++) {
Math.random();
}
});
jest.useFakeTimers();
scheduler.start(heavyTask.name);
await jest.advanceTimersByTimeAsync(1000);
setTimeout(() => {}, 1000);
});
});
|
FakeTimers는 정확히 1초 후 실행되겠지만 실제 환경에서는 CPU 부하로 인해 1초보다 더 늦게 실행될 수 있습니다.
이벤트 루프 블로킹
실제 환경에서는 이벤트 루프 블로킹이 타이머 실행에 영향을 미칠 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| describe('이벤트 루프 블로킹', () => {
it('블로킹 상황의 차이', async () => {
const task = createMockTask('task', 1000);
const tasks = [task];
const scheduler = new Scheduler(tasks);
// FakeTimers 환경
jest.useFakeTimers();
scheduler.start(task.name);
// 이벤트 루프를 블로킹하는 동기 작업
const blockingOperation = () => {
const start = Date.now();
while (Date.now() - start < 500) {
} // 500ms 동안 블로킹
};
blockingOperation();
await jest.advanceTimersByTimeAsync(100);
expect(task.execute).toHaveBeenCalledTimes(1); // 성공
});
});
|
위와 같은 코드는 테스트 환경에서 성공하지만, 실제 환경에서는 실패하는 테스트 입니다.
- 블로킹 동안 타이머가 지연됨
- 블로킹이 끝난 후 밀린 타이머가 실행됨
- 정확한 간격으로 실행되지 않을 수 있음
해결 방안
중요한 시간 관련 로직은 실제 환경에서도 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| describe('실제 환경 테스트', () => {
it('중요한 타이밍 테스트', (done) => {
const task = createMockTask('task', 1000);
const tasks = [task];
const scheduler = new Scheduler(tasks);
// 실제 타이머로 테스트
scheduler.start(task.name);
setTimeout(() => {
try {
expect(task.execute).toHaveBeenCalled();
scheduler.stop(task.name);
done();
} catch (error) {
done(error);
}
}, 1500); // 여유 있는 타임아웃 설정
});
});
|
테스트에 적절한 여유 시간 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| describe('여유 시간이 있는 테스트', () => {
it('안정적인 타이밍 테스트', (done) => {
const task = createMockTask('task', 1000);
const tasks = [task];
const scheduler = new Scheduler(tasks);
scheduler.start(task.name);
// 1초 간격으로 3번 실행 체크 시
setTimeout(() => {
try {
expect(task.execute).toHaveBeenCalledTimes(3);
scheduler.stop(task.name);
done();
} catch (error) {
done(error);
}
}, 3500); // 3초가 아닌 3.5초로 여유 시간 추가
});
});
|
마무리
Jest의 FakeTimers를 활용하면 시간 관련 코드를 안정적이고 빠르게 테스트할 수 있습니다. 특히 스케줄러와 같이 시간에 의존적인 로직을 테스트할 때 매우 유용한 도구입니다.
실제 시간에 의존하지 않고 시간을 제어할 수 있다는 점은 테스트의 신뢰성과 효율성을 크게 향상시킵니다. 다만, 비동기 처리와 클린업에 주의를 기울여야 하며, 필요한 경우 실제 환경에서의 검증도 병행하는 것이
좋겠습니다.
끝까지 읽어주셔서 감사합니다 😊