React Native中jest.useFakeTimers在beforeEach内外调用差异问题
概述
测试过程中发现,jest.useFakeTimers 在 beforeEach 钩子内部调用、与在钩子外部调用时,存在不一致的运行行为。
可复现示例
// Foo.tsx import * as React from 'react'; import {View} from 'react-native'; const Foo: React.FC = () => { const [timePassed, setTimePassed] = React.useState<number>(0); const [flag, setFlag] = React.useState(true); console.log(`timePassed: ${timePassed}, flag: ${flag}`); React.useEffect(() => { if (flag) { setTimeout(() => { if (timePassed > 1) { setFlag(false); } setTimePassed(timePassed + 1); }, 1); } }, [flag, timePassed]); return <View />; }; export default Foo;
// foo.outside.test.tsx import * as React from 'react'; import {render} from '@testing-library/react-native'; import Foo from '../Foo'; jest.useFakeTimers(); describe('Test Foo', () => { test('1', () => { render(<Foo />); jest.runAllTimers(); }); test('2', () => { render(<Foo />); jest.runAllTimers(); }); });
// foo.inside.test.tsx import * as React from 'react'; import {render} from '@testing-library/react-native'; import Foo from '../Foo'; describe('Test Foo', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.runOnlyPendingTimers(); jest.useRealTimers(); }); test('1', () => { render(<Foo />); jest.runAllTimers(); }); test('2', () => { render(<Foo />); jest.runAllTimers(); }); });
测试结果
外部调用场景
$ npx jest __tests__/foo.outside.test.tsx console.log timePassed: 0, flag: true at log (Foo.tsx:8:11) console.log timePassed: 1, flag: true at log (Foo.tsx:8:11) console.log timePassed: 2, flag: true at log (Foo.tsx:8:11) console.log timePassed: 2, flag: false at log (Foo.tsx:8:11) console.log timePassed: 3, flag: false at log (Foo.tsx:8:11) console.log timePassed: 0, flag: true at log (Foo.tsx:8:11) console.log timePassed: 1, flag: true at log (Foo.tsx:8:11) console.log timePassed: 2, flag: true at log (Foo.tsx:8:11) console.log timePassed: 2, flag: false at log (Foo.tsx:8:11) console.log timePassed: 3, flag: false at log (Foo.tsx:8:11) PASS __tests__/foo.outside.test.tsx Test Foo ✓ 1 (58 ms) ✓ 2 (21 ms)
内部调用场景
$ npx jest __tests__/foo.inside.test.tsx console.log timePassed: 0, flag: true at log (Foo.tsx:8:11) console.log timePassed: 1, flag: true at log (Foo.tsx:8:11) console.log timePassed: 2, flag: true at log (Foo.tsx:8:11) console.log timePassed: 2, flag: false at log (Foo.tsx:8:11) console.log timePassed: 3, flag: false at log (Foo.tsx:8:11) console.log timePassed: 0, flag: true at log (Foo.tsx:8:11) console.log timePassed: 1, flag: true at log (Foo.tsx:8:11) PASS __tests__/foo.inside.test.tsx Test Foo ✓ 1 (62 ms) ✓ 2 (14 ms)
现象观察
经测试可见:外部调用配置下,两个测试用例中的 jest.runAllTimers 均可将定时器完整执行到终止状态;但内部调用配置下,仅第一个测试用例的定时器执行到了终止状态,第二个测试用例的定时器提前退出,未完整执行。
问题原因
两种写法的行为差异,核心是afterEach中的清理逻辑和Jest假定时器、React调度器的工作机制不匹配导致的:
- 全局外部调用
jest.useFakeTimers()时,假定时器在整个文件测试周期内持续生效,两个用例之间不会切换回真实定时器,也没有额外的提前执行逻辑。每个用例调用jest.runAllTimers()时,会递归执行所有已注册、以及执行过程中新产生的定时任务,直到队列完全清空,所以两个用例的定时器都能跑到终止状态。 - 写在
beforeEach里的场景问题出在afterEach的逻辑:- 第一个用例执行完后,
jest.runOnlyPendingTimers()只会执行调用瞬间已经在队列里的定时任务,不会处理任务执行过程中新注册的定时器,跑完这一步就直接切回真实定时器,第一个用例中React触发的后续状态更新、连带新的定时任务没有被清空,残留在了运行环境中。 - 第二个用例启动时,虽然
beforeEach重新开启了假定时器,但React内部的调度器、React Native测试环境的宿主对象已经缓存了上一步切回的真实定时器引用,后续组件里setTimeout注册的任务不会进入新的假定时器队列。此时调用jest.runAllTimers()只能拿到队列里最开始的两个定时任务,跑完就直接退出,表现就是定时器没有执行完整。
- 第一个用例执行完后,
修复方式很简单,删掉afterEach里的jest.runOnlyPendingTimers()即可。每次调用jest.useFakeTimers()本身就会重置假定时器队列,不需要额外在清理阶段执行残留任务。如果需要主动规避用例间的状态污染,可以在beforeEach里加一句jest.clearAllTimers()主动清空队列即可。
运行环境信息
- 操作系统:macOS Big Sur v11.6.5
- react: 17.0.2
- react-native: 0.65.1
- jest: 28.1.2
- @testing-library/react-native: 10.1.1
- typescript: 4.5.2
内容的提问来源于stack exchange,提问作者Fanchen Bao
相关产品推荐
相关产品推荐

