Jest 中 如何测试 setTimeout

Jest Time Mocks

原生的 time 函数 (比如,setTimeout, setInterval, clearTimeout, clearInterval) 不是很便于测试,因为需要依赖于真正的时间流逝,Jest 可以用函数来代替 timers, 从而让你来控制时间的流逝。

假设我们需要测试一段异步的代码:

function fun(obj) {
  setTimeout(() => {
    obj.a = 1;
  }, 1000);
}


test('test', () => {
  const obj = { a: 2 };
  fun(obj);
  expect(obj).toEqual({ a: 1 });   // error
});

以上代码会失败,因为 setTimeout 是异步代码,需要一段时间之后才能得到结果.

于是 Jest 提供了 runAllTimers() 这个函数来测试异步。

function fun(obj) {
  setTimeout(() => {
    obj.a = 1;
  }, 1000);
}


test('test', () => {
  const obj = { a: 2 };
  jest.useFakeTimers();        //  开启模拟定时器
  fun(obj);
  jest.runAllTimers();         // 加速,让所有的定时器都执行完毕
  expect(obj).toEqual({ a: 1 });
});

通过以上这种方式,异步代码就可以测试通过了。

但是,如果存在 timer 递归的时候runAllTimer() 就会失败,因为一直都存在定时器。

let a = 1;
let b = 2;

function fun() {
  setTimeout(() => {
    a = 2;
    setTimeout(() => {
      b = 3;
      fun();        // 定时器里递归调用定时器
    }, 2000);
  }, 1000);
}

// error, 不能使用 runAllTimers 方法
test('test', () => {
  jest.useFakeTimers();
  fun();
  jest.runAllTimers();
  expect(a).toBe(2);
  expect(b).toBe(3);
});

这个时候应该使用 runOnlyPendingTimers

let a = 1;
let b = 2;

function fun() {
  setTimeout(() => {
    a = 2;
    setTimeout(() => {
      b = 3;
      fun();
    }, 2000);
  }, 1000);
}

test('test', () => {
  jest.useFakeTimers();
  expect(a).toBe(1);
  expect(b).toBe(2);
  fun();
  jest.runOnlyPendingTimers();  // 加速第一个定时器
  expect(a).toBe(2);            // 正确
  expect(b).toBe(3);            // 错误,第二个定时器还没执行
  jest.runOnlyPendingTimers();  // 加速第二个定时器
  expect(b).toBe(3);            // 正确
});

另外,我们可以通过 advanceTimersByTime(time) 来指定加速多久。

还是这个例子:

let a = 1;
let b = 2;

function fun() {
  setTimeout(() => {
    a = 2;
    setTimeout(() => {
      b = 3;
      fun();
    }, 2000);
  }, 1000);
}


// 错误
// 即当我们加速 1s 的时候,只有第一个定时器会执行
test('test', () => {
  jest.useFakeTimers();
  fun();
  jest.advanceTimersByTime(1000);
  expect(a).toBe(2);
  expect(b).toBe(3);   // 应该是 2
});

// 正确
// 而当我们加速 3s 的时候,两个定时器都会执行
// 因此 a, b 的值都会发生变化
test('test', () => {
  jest.useFakeTimers();
  fun();
  jest.advanceTimersByTime(3000);
  expect(a).toBe(2);
  expect(b).toBe(3);
});

以上定时器的测试例子,同样适用于测试 React 组件,如某个操作发生,需要 setTimeout 一定时间之后才改变 state 的场景。

Last updated