You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 22:32:43