测试时如何简易将Rust的thread_rng()替换为带种子的确定性随机生成器
低成本实现测试场景下确定性随机数的方案
rand::thread_rng() 本身没有开放全局替换实现、固定种子的公开API,官方刻意没有提供这类配置——毕竟它的设计定位是生产环境使用的加密安全非确定性随机数生成器,开放全局修改入口会引入不可控的安全风险。
不需要给数百个函数新增参数透传Rng实例,用下面的方案就能以极低的改造成本实现测试场景的确定性随机:
- 做一层极薄的全局封装,通过条件编译区分正式/测试环境的Rng实现
只需要在项目里新增一个公共模块,代码如下:
改造成本只有一步:全局正则搜索替换所有pub mod rng { #[cfg(not(test))] pub use rand::thread_rng; #[cfg(test)] use std::cell::RefCell; #[cfg(test)] use rand::rngs::StdRng; #[cfg(test)] use rand::{CryptoRng, Error, RngCore, SeedableRng}; #[cfg(test)] thread_local! { static TEST_RNG: RefCell<StdRng> = RefCell::new(StdRng::seed_from_u64(42)); } #[cfg(test)] pub struct TestThreadRng; #[cfg(test)] impl RngCore for TestThreadRng { #[inline] fn next_u32(&mut self) -> u32 { TEST_RNG.with(|rng| rng.borrow_mut().next_u32()) } #[inline] fn next_u64(&mut self) -> u64 { TEST_RNG.with(|rng| rng.borrow_mut().next_u64()) } #[inline] fn fill_bytes(&mut self, dest: &mut [u8]) { TEST_RNG.with(|rng| rng.borrow_mut().fill_bytes(dest)) } #[inline] fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { TEST_RNG.with(|rng| rng.borrow_mut().try_fill_bytes(dest)) } } // 如果你的代码对Rng有CryptoRng trait约束就保留这行,否则可以删掉 #[cfg(test)] impl CryptoRng for TestThreadRng {} #[cfg(test)] pub fn thread_rng() -> TestThreadRng { TestThreadRng } /// 测试专用:重置当前线程的随机数种子,保证单测内随机序列确定 #[cfg(test)] pub fn reset_seed(seed: u64) { TEST_RNG.with(|rng| *rng.borrow_mut() = StdRng::seed_from_u64(seed)); } }rand::thread_rng()调用为crate::rng::thread_rng(),不需要修改任何函数签名、业务逻辑,正式运行时和原有逻辑完全一致,没有任何性能损耗。 - 测试场景使用说明
单测中如果需要固定随机序列,只需要在测试用例开头调用rng::reset_seed(你指定的固定种子)即可。由于测试Rng存在thread_local存储中,多线程并行跑测试时不同线程的随机序列互不干扰,不会出现串扰。
注意:不要尝试通过unsafe指针修改原生
thread_rng()的内部状态,rand不同版本的内部实现变动极大,这类操作会直接引发未定义行为,稳定性完全无法保证。如果个别测试需要真实的非确定性随机数,直接调用原生rand::thread_rng()即可,和封装的测试Rng不冲突。
内容的提问来源于stack exchange,提问作者Derek Thurn
相关产品推荐
相关产品推荐

