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

如何为带掩码的ReadLine方法编写单元测试?

如何为带掩码的ReadLine方法编写单元测试

我要给一个带掩码的ReadLine方法写单元测试,这个方法用Console.ReadKey(true)隐藏输入字符并显示掩码。之前用重定向Console输入输出流的方式测试普通ReadLine包装类没问题,但这个掩码方法不行——Console.ReadKey只会响应实际按键,不认传入的字符串流,导致测试一直挂起等待按键。试过SendKey但单元测试项目没法引入Forms库,想找不依赖实际按键的实现或模拟方案。


待测试的掩码ReadLine方法

public string ReadLineMasked()
{
    Console.Write(Prompt); // Prompt是类的属性

    ConsoleKey pressedKey;
    var userInput = new StringBuilder();

    do
    {
        var keyInfo = Console.ReadKey(true);
        pressedKey = keyInfo.Key;

        if (pressedKey == ConsoleKey.Backspace && userInput.Length > 0)
        {
            Console.Write("\b \b");
            userInput.Length--;
        }
        else if (!char.IsControl(keyInfo.KeyChar))
        {
            Console.Write(MaskChar);
            userInput.Append(keyInfo.KeyChar);
        }

    } while (pressedKey != ConsoleKey.Enter);

    Console.WriteLine();
    return userInput.ToString();
}

普通ReadLine的测试代码(对掩码方法无效)

public void ReadLineTests(string UserInput)
{
    using var WriteStream = StreamHelper.GetWriterStream();
    using var ReadSource = StreamHelper.GetReaderStream(UserInput);
    
    Console.SetOut(WriteStream); // 接管控制台输出
    Console.SetIn(ReadSource); // 接管输入源,模拟文本输入

    var sut = new SystemConsoleReader();
    var inputResult = sut.ReadLine();
    var writtenResult = StreamHelper.GetStringFromStreamWriter(WriteStream);

    // 验证输出的命令提示符和输入结果是否符合预期
    Assert.Equal("TCLI> ", writtenResult);
    Assert.Equal(UserInput, inputResult);
}

辅助方法代码

public static string GetStringFromStreamWriter(StreamWriter writer)
{
    writer.Flush(); // 读取前必须先Flush
    writer.BaseStream.Position = 0; // 将流指针重置到开头
    using var reader = new StreamReader(writer.BaseStream);
    var recoveredValue = reader.ReadToEnd();
    writer.Close();
    writer.Dispose();
    return recoveredValue;
}

解决方案

方案1:抽象Console操作接口+依赖注入(推荐)

核心思路是把Console的操作抽象成接口,测试时用模拟实现替代真实Console,这是最可靠且符合设计原则的方案。

步骤1:定义Console操作接口

public interface IConsole
{
    void Write(char value);
    void Write(string value);
    void WriteLine();
    ConsoleKeyInfo ReadKey(bool intercept);
    string Prompt { get; set; }
    char MaskChar { get; set; }
}

步骤2:实现真实Console的包装类

public class SystemConsole : IConsole
{
    public string Prompt { get; set; }
    public char MaskChar { get; set; }

    public void Write(char value) => Console.Write(value);
    public void Write(string value) => Console.Write(value);
    public void WriteLine() => Console.WriteLine();
    public ConsoleKeyInfo ReadKey(bool intercept) => Console.ReadKey(intercept);
}

步骤3:修改掩码方法依赖接口

public class SystemConsoleReader
{
    private readonly IConsole _console;

    // 通过构造函数注入IConsole实例
    public SystemConsoleReader(IConsole console)
    {
        _console = console;
    }

    public string ReadLineMasked()
    {
        _console.Write(_console.Prompt);

        ConsoleKey pressedKey;
        var userInput = new StringBuilder();

        do
        {
            var keyInfo = _console.ReadKey(true);
            pressedKey = keyInfo.Key;

            if (pressedKey == ConsoleKey.Backspace && userInput.Length > 0)
            {
                _console.Write("\b \b");
                userInput.Length--;
            }
            else if (!char.IsControl(keyInfo.KeyChar))
            {
                _console.Write(_console.MaskChar);
                userInput.Append(keyInfo.KeyChar);
            }

        } while (pressedKey != ConsoleKey.Enter);

        _console.WriteLine();
        return userInput.ToString();
    }
}

步骤4:编写单元测试(以Moq为例)

[Test]
public void ReadLineMasked_ShouldReturnCorrectInput_WithMaskOutput()
{
    // 准备测试按键序列:输入"abc" → 退格 → 输入"d" → 回车
    var keySequence = new List<ConsoleKeyInfo>
    {
        new ConsoleKeyInfo('a', ConsoleKey.A, false, false, false),
        new ConsoleKeyInfo('b', ConsoleKey.B, false, false, false),
        new ConsoleKeyInfo('c', ConsoleKey.C, false, false, false),
        new ConsoleKeyInfo('\b', ConsoleKey.Backspace, false, false, false),
        new ConsoleKeyInfo('d', ConsoleKey.D, false, false, false),
        new ConsoleKeyInfo('\r', ConsoleKey.Enter, false, false, false)
    };

    var mockConsole = new Mock<IConsole>();
    mockConsole.SetupProperty(c => c.Prompt, "TCLI> ");
    mockConsole.SetupProperty(c => c.MaskChar, '*');
    
    // 模拟ReadKey按顺序返回预设按键
    var keyIndex = 0;
    mockConsole.Setup(c => c.ReadKey(true))
               .Returns(() => keySequence[keyIndex++]);

    var sut = new SystemConsoleReader(mockConsole.Object);
    var result = sut.ReadLineMasked();

    // 验证最终输入结果正确
    Assert.Equal("abd", result);

    // 验证输出行为符合预期
    mockConsole.Verify(c => c.Write("TCLI> "), Times.Once);
    mockConsole.Verify(c => c.Write('*'), Times.Exactly(4)); // abc3个+ d1个
    mockConsole.Verify(c => c.Write("\b \b"), Times.Once);
    mockConsole.Verify(c => c.WriteLine(), Times.Once);
}

方案2:反射修改Console内部状态(不推荐)

如果不想修改原有代码结构,可以用反射绕过Console.ReadKey的限制,但这种方法依赖.NET内部实现,版本更新后可能失效:

public static void SimulateConsoleKeyPresses(List<ConsoleKeyInfo> keyInfos)
{
    var consoleType = typeof(Console);
    var internalField = consoleType.GetField("_in", BindingFlags.NonPublic | BindingFlags.Static);
    var customReader = new MockedConsoleReader(keyInfos);
    internalField.SetValue(null, customReader);
}

// 自定义TextReader子类模拟按键输入
public class MockedConsoleReader : TextReader
{
    private readonly Queue<ConsoleKeyInfo> _keys;

    public MockedConsoleReader(List<ConsoleKeyInfo> keys)
    {
        _keys = new Queue<ConsoleKeyInfo>(keys);
    }

    // 重写ReadKey相关逻辑(注意:Console.ReadKey实际不使用TextReader,此方法仅作示例)
    public override int Read()
    {
        return _keys.Dequeue().KeyChar;
    }
}

方案3:使用第三方Console测试库

可以引入专门的测试库如ConsoleTestKit,这类库封装了Console模拟逻辑,无需手动实现抽象层,但需要额外添加依赖。


总结

最可靠且可维护的方案是方案1的抽象接口+依赖注入,它不依赖系统内部实现,测试逻辑清晰,也符合SOLID设计原则。方案2兼容性差,仅适合临时测试场景。

内容的提问来源于stack exchange,提问作者Kevin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 09:14:54