如何避免Rust测试中重复编写MockStdout结构体代码?
问题描述
在多个Rust测试文件中,存在一段重复的MockStdout结构体及其Write trait实现代码:
#[cfg(test)] mod tests { use std::io::Write; use super::*; #[derive(Default, Debug)] pub struct MockStdout { pub buffer: Vec<u8>, pub cursor_pos: (u16, u16), } impl MockStdout { pub fn new() -> Self { let buffer = Vec::new(); Self { buffer, cursor_pos: (1, 1), } } } impl Write for MockStdout { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { self.buffer.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.buffer.flush() } }
但每个测试文件中,MockStdout的TermCursor trait实现逻辑各不相同:
impl TermCursor for MockStdout { fn write_term(&mut self, fmt: std::fmt::Arguments) -> std::io::Result<()> { // 不同的实现逻辑 } } }
尝试将重复代码抽离到mock_stdout.rs公共模块时,遇到两个问题:
- a. 多个文件中为同一个
MockStdout实现TermCursor会引发实现冲突; - b. 用
MyMockStdout { stdout: MockStdout }包裹后,现有代码需要修改为stdout.stdout才能访问原字段/方法,侵入性强。
最优解决方案
以下三种方案均可解决问题,可根据项目复杂度和个人习惯选择:
方案1:Deref适配器实现专属Mock(推荐)
把通用的MockStdout放在公共模块,每个测试文件定义自己的适配器结构体,通过Deref/DerefMut让适配器直接拥有原结构体的所有能力,同时实现专属的TermCursor。
步骤1:公共模块mock_stdout.rs
#[cfg(test)] use std::io::Write; #[derive(Default, Debug)] pub struct MockStdout { pub buffer: Vec<u8>, pub cursor_pos: (u16, u16), } impl MockStdout { pub fn new() -> Self { Self { buffer: Vec::new(), cursor_pos: (1, 1), } } } impl Write for MockStdout { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { self.buffer.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.buffer.flush() } }
步骤2:测试文件中定义专属Mock
#[cfg(test)] mod tests { use std::ops::{Deref, DerefMut}; use crate::mock_stdout::MockStdout; use super::*; // 每个测试文件定义自己的专属Mock,名字可对应测试场景 struct LoginTestMock(MockStdout); // 通过Deref让适配器直接代理原MockStdout的字段和方法 impl Deref for LoginTestMock { type Target = MockStdout; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for LoginTestMock { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } // 实现当前测试文件专属的TermCursor逻辑 impl TermCursor for LoginTestMock { fn write_term(&mut self, fmt: std::fmt::Arguments) -> std::io::Result<()> { // 自定义逻辑:比如修改光标位置并写入日志 self.cursor_pos = (1, 20); writeln!(&mut self.buffer, "[LoginTest] {}", fmt)?; Ok(()) } } // 测试用例用法和原MockStdout完全一致 #[test] fn test_login_prompt() { let mut mock = LoginTestMock(MockStdout::new()); mock.write(b"Enter username: ")?; mock.write_term(format_args!("Cursor moved to {:?}", mock.cursor_pos))?; // 验证逻辑 assert_eq!(mock.cursor_pos, (1, 20)); } }
该方案完美解决两个问题:专属Mock的TermCursor实现不会冲突,且现有代码无需修改调用方式。
方案2:注入式Handler解耦逻辑
让公共的MockStdout持有一个Handler trait对象,将write_term的逻辑委托给Handler,每个测试文件实现自己的Handler即可。
步骤1:公共模块mock_stdout.rs
#[cfg(test)] use std::io::Write; use std::fmt::Arguments; // 定义Handler trait,抽象write_term的逻辑 pub trait TermCursorHandler: Send + Sync { fn handle_write_term(&mut self, mock: &mut MockStdout<Self>, fmt: Arguments) -> std::io::Result<()>; } #[derive(Debug)] pub struct MockStdout<H: TermCursorHandler> { pub buffer: Vec<u8>, pub cursor_pos: (u16, u16), handler: H, } impl<H: TermCursorHandler> MockStdout<H> { pub fn new(handler: H) -> Self { Self { buffer: Vec::new(), cursor_pos: (1, 1), handler, } } } impl<H: TermCursorHandler> Write for MockStdout<H> { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { self.buffer.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.buffer.flush() } } // 统一实现TermCursor,委托给Handler impl<H: TermCursorHandler> TermCursor for MockStdout<H> { fn write_term(&mut self, fmt: Arguments) -> std::io::Result<()> { self.handler.handle_write_term(self, fmt) } }
步骤2:测试文件中实现Handler
#[cfg(test)] mod tests { use crate::mock_stdout::{MockStdout, TermCursorHandler}; use super::*; struct LogoutTestHandler; impl TermCursorHandler for LogoutTestHandler { fn handle_write_term(&mut self, mock: &mut MockStdout<Self>, fmt: Arguments) -> std::io::Result<()> { mock.cursor_pos = (5, 1); writeln!(&mut mock.buffer, "[LogoutTest] {}", fmt)?; Ok(()) } } #[test] fn test_logout_confirm() { let mut mock = MockStdout::new(LogoutTestHandler); mock.write(b"Confirm logout? (Y/N): ")?; mock.write_term(format_args!("Cursor reset"))?; } }
该方案适合需要复杂逻辑复用的场景,核心Mock代码无需修改,只需扩展Handler即可。
方案3:宏生成专属Mock
用宏封装适配器的重复代码,每个测试文件只需一行宏调用即可生成带有自定义TermCursor的Mock结构体。
步骤1:公共模块定义宏
#[cfg(test)] #[macro_export] macro_rules! create_mock_stdout { ($mock_name:ident, $write_term_impl:block) => { struct $mock_name(crate::mock_stdout::MockStdout); impl std::ops::Deref for $mock_name { type Target = crate::mock_stdout::MockStdout; fn deref(&self) -> &Self::Target { &self.0 } } impl std::ops::DerefMut for $mock_name { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl crate::TermCursor for $mock_name { fn write_term(&mut self, fmt: std::fmt::Arguments) -> std::io::Result<()> { $write_term_impl } } impl $mock_name { pub fn new() -> Self { Self(crate::mock_stdout::MockStdout::new()) } } }; }
步骤2:测试文件中调用宏
#[cfg(test)] mod tests { use create_mock_stdout; use super::*; // 一行代码生成专属Mock并实现自定义write_term create_mock_stdout!(SettingsTestMock, { self.cursor_pos = (3, 15); writeln!(&mut self.buffer, "[SettingsTest] {}", fmt)?; Ok(()) }); #[test] fn test_settings_nav() { let mut mock = SettingsTestMock::new(); mock.write(b"Select setting: ")?; mock.write_term(format_args!("Moved to display settings"))?; } }
该方案最简洁,适合大量测试文件需要快速生成专属Mock的场景。
内容的提问来源于stack exchange,提问作者wyc
相关产品推荐
相关产品推荐

