【问题标题】:C# unit test for a method which calls Console.ReadLine()调用 Console.ReadLine() 的方法的 C# 单元测试
【发布时间】:2011-03-10 20:00:47
【问题描述】:

我想为名为@9​​87654321@ 的类的成员函数创建一个单元测试,该类存储游戏中排名前五的玩家。

问题是我为 (SignInScoreBoard) 创建测试的方法正在调用 Console.ReadLine(),因此用户可以输入他们的姓名:

public void SignInScoreBoard(int steps)
{
    if (topScored.Count < 5)
    {
        Console.Write(ASK_FOR_NAME_MESSAGE);
        string name = Console.ReadLine();
        KeyValuePair<string, int> pair = new KeyValuePair<string, int>(name, steps);
        topScored.Insert(topScored.Count, pair);
    }
    else
    {
        if (steps < topScored[4].Value)
        {
            topScored.RemoveAt(4);
            Console.Write(ASK_FOR_NAME_MESSAGE);
            string name = Console.ReadLine();
            topScored.Insert(4, new KeyValuePair<string, int>(name, steps));
        }
    }
}

有没有办法插入十个用户,这样我就可以检查是否存储了五个移动(步数)较少的用户?

【问题讨论】:

    标签: c# visual-studio unit-testing console console-redirect


    【解决方案1】:

    您需要将调用 Console.ReadLine 的代码行重构为一个单独的对象,以便您可以在测试中使用自己的实现将其存根。

    举个简单的例子,你可以像这样创建一个类:

    public class ConsoleNameRetriever {
         public virtual string GetNextName()
         {
             return Console.ReadLine();
         }
    }
    

    然后,在您的方法中,将其重构为采用此类的实例。但是,在测试时,您可以使用测试实现覆盖它:

    public class TestNameRetriever : ConsoleNameRetriever {
         // This should give you the idea...
         private string[] names = new string[] { "Foo", "Foo2", ... };
         private int index = 0;
         public override string GetNextName()
         {
             return names[index++];
         }
    }
    

    当您进行测试时,用测试实现替换实现。

    当然,我个人会使用框架来简化此操作,并使用干净的接口而不是这些实现,但希望以上内容足以为您提供正确的想法...

    【讨论】:

    • 我完全不同意。仅仅为了测试而重构是一个坏主意。查看 Microsoft Moles 以获得更好的解决方案。
    • @Stephen Cleary 我不可能再不同意你的观点了。您不仅应该争取可测试的代码,而且关注点分离不仅仅是一个测试关注点。这种重构将为他留下一个更加模块化、可重用的代码库。它还使他能够编写干净、简单的单元测试。
    • @Stephen:我不会“仅仅为了测试”而重构它——我会重构它,因为它有多重责任,而可测试性将是重构的一个简单副作用。此方法过于复杂,无法有效测试。
    • 关注点分离是一个很好的设计目标,但太多只会造成巨大的混乱。单元测试并不比使用 Moles 更干净或更简单。重构不会给他留下一个更加模块化、可重用的代码库——仅仅因为在重用之前必须理解代码,而完全不必要的控制台抽象变得复杂了代码相当多。
    • @Stephen:我认为我们必须同意在这里不同意。我觉得用户数据的输入(和输出,就此而言)应该总是从方法中抽象出来。将 Console.ReadLine 放在一个除了处理用户输入之外什么都做的方法中,这对我来说真的很糟糕......
    【解决方案2】:

    您应该重构您的代码以从该代码中移除对控制台的依赖。

    例如,您可以这样做:

    public interface IConsole
    {
        void Write(string message);
        void WriteLine(string message);
        string ReadLine();
    }
    

    然后像这样更改您的代码:

    public void SignInScoreBoard(int steps, IConsole console)
    {
        ... just replace all references to Console with console
    }
    

    要在生产环境中运行,请将此类的实例传递给它:

    public class ConsoleWrapper : IConsole
    {
        public void Write(string message)
        {
            Console.Write(message);
        }
    
        public void WriteLine(string message)
        {
            Console.WriteLine(message);
        }
    
        public string ReadLine()
        {
            return Console.ReadLine();
        }
    }
    

    但是,在测试时,使用这个:

    public class ConsoleWrapper : IConsole
    {
        public List<String> LinesToRead = new List<String>();
    
        public void Write(string message)
        {
        }
    
        public void WriteLine(string message)
        {
        }
    
        public string ReadLine()
        {
            string result = LinesToRead[0];
            LinesToRead.RemoveAt(0);
            return result;
        }
    }
    

    这使您的代码更容易测试。

    当然,如果您还想检查是否写入了正确的输出,则需要在 write 方法中添加代码以收集输出,以便您可以在测试代码中对其进行断言。

    【讨论】:

    • +1,IMO 这是您必须遵循的结构。但是,我会更抽象地思考,而不是考虑控制台并包装它,我会使用 INameProvider 接口和 GetName() 方法。然后,可以有 ConsoleNameProvider、HardCodedNameProvider、WhateverNameProvider 等实现。
    【解决方案3】:

    您可以使用MolesConsole.ReadLine 替换为您自己的方法,而无需更改您的代码(完全不需要设计和实现一个支持依赖注入的抽象控制台)。

    【讨论】:

      【解决方案4】:

      为什么不为标准输入和标准输出创建一个新的流(文件/内存),然后在调用方法之前将输入/输出重定向到新的流?然后,您可以在方法完成后检查流的内容。

      【讨论】:

      • 顺便说一句,我根本不喜欢这种方法 :) 我更喜欢像其他人建议的那样抽象控制台。
      【解决方案5】:
      public void SignInScoreBoard(int steps, Func<String> nameProvider)
      {
          ...
              string name = nameProvider();
          ...
      }  
      

      在您的测试用例中,您可以将其称为

      SignInScoreBoard(val, () => "TestName");
      

      在您的正常实现中,将其称为

      SignInScoreBoard(val, Console.ReadLine);
      

      如果您使用的是 C# 4.0,您可以通过以下方式将 Console.ReadLine 设为默认值

      public void SignInScoreBoard(int steps, Func<String> nameProvider=null)
      {
          nameProvider = nameProvider ?? Console.ReadLine;
          ...
      

      【讨论】:

      • 函数式编程范式的伟大使用!优雅且可测试。
      【解决方案6】:

      与其抽象Console,我更愿意创建一个组件来封装这个逻辑,并测试这个组件,并在控制台应用程序中使用它。

      【讨论】:

        【解决方案7】:

        我无法相信有多少人在没有正确查看问题的情况下回答了问题。问题是所讨论的方法不止一件事,即询问名称并插入最高分。任何对控制台的引用都可以从此方法中取出,而应该传入名称:

        public void SignInScoreBoard(int steps, string nameOfTopScorer)
        

        对于其他测试,您可能希望按照其他答案中的建议抽象出控制台输出的读数。

        【讨论】:

        • 实际上,如果他们的得分超过了得分最高的人,它只需要名字,所以总是传递名字是不合适的,这就是为什么所有其他答案都暗示了 NameProvider 的一些变体 在适当的时候询问。
        【解决方案8】:

        几天前我遇到了类似的问题。封装控制台类对我来说似乎有点矫枉过正。基于 KISS 原则和 IoC/DI 原则,我将写入器(输出)和读取器(输入)的依赖关系放入构造函数。让我举个例子。

        我们可以假设由接口IConfirmationProvider定义的简单确认提供者

        public interface IConfirmationProvider
        {
            bool Confirm(string operation);
        }
        

        他的实现是

        public class ConfirmationProvider : IConfirmationProvider
        {
            private readonly TextReader input;
            private readonly TextWriter output;
        
            public ConfirmationProvider() : this(Console.In, Console.Out)
            {
        
            }
        
            public ConfirmationProvider(TextReader input, TextWriter output)
            {
                this.input = input;
                this.output = output;
            }
        
            public bool Confirm(string operation)
            {
                output.WriteLine($"Confirmed operation {operation}...");
                if (input.ReadLine().Trim().ToLower() != "y")
                {
                    output.WriteLine("Aborted!");
                    return false;
                }
        
                output.WriteLine("Confirmated!");
                return true;
            }
        }
        

        现在,当您向 TextWriterTextReader 注入依赖项时,您可以轻松测试您的实现(在此示例中,StreamReaderTextReader

        [Test()]
        public void Confirm_Yes_Test()
        {
            var cp = new ConfirmationProvider(new StringReader("y"), Console.Out);
            Assert.IsTrue(cp.Confirm("operation"));
        }
        
        [Test()]
        public void Confirm_No_Test()
        {
            var cp = new ConfirmationProvider(new StringReader("n"), Console.Out);
            Assert.IsFalse(cp.Confirm("operation"));
        }
        

        并使用默认应用程序标准方式的实现(Console.InTextReaderConsole.OutTextWriter

        IConfirmationProvider cp = new ConfirmationProvider();
        

        就是这样 - 一个额外的带有字段初始化的 ctor。

        【讨论】:

          【解决方案9】:

          您不应该模拟来自框架的东西,.NET 已经为其组件提供了抽象。对于控制台,它们是方法 Console.SetIn() 和 Console.SetOut()。

          例如,对于 Console.Readline(),您可以这样做:

          [TestMethod]
          MyTestMethod()
          {
              Console.SetIn(new StringReader("fakeInput"));
              var result = MyTestedMethod();
              StringAssert.Equals("fakeInput", result);
          }
          

          考虑到被测试的方法返回由 Console.Readline() 读取的输入。该方法将使用我们设置为控制台输入的字符串,而不是等待交互式输入。

          【讨论】:

            猜你喜欢
            • 2023-03-06
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-12-21
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多