调用Console.ReadLine()的方法的C#单元测试

Pet*_*bev 11 c# console unit-testing visual-studio console-redirect

我想为一个类的成员函数创建一个单元测试,该类函数ScoreBoard存储游戏中的前五名玩家.

问题是我为(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));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Ree*_*sey 24

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

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

public class ConsoleNameRetriever {
     public virtual string GetNextName()
     {
         return Console.ReadLine();
     }
}
Run Code Online (Sandbox Code Playgroud)

然后,在您的方法中,重构它以取代此类的实例.但是,在测试时,您可以使用测试实现覆盖它:

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++];
     }
}
Run Code Online (Sandbox Code Playgroud)

测试时,使用测试实现交换实现.

当然,我个人会使用一个框架来使这更容易,并使用一个干净的界面而不是这些实现,但希望上面的内容足以给你正确的想法......

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

ang*_*son 14

您应该重构代码以从此代码中删除对控制台的依赖性.

例如,你可以这样做:

public interface IConsole
{
    void Write(string message);
    void WriteLine(string message);
    string ReadLine();
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,在测试时,请使用:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • +1,IMO 这是您必须遵循的结构。但是,我会更抽象地思考,而不是考虑控制台并将其包装起来,我会使用 INameProvider 接口和 GetName() 方法。然后,您可以拥有 ConsoleNameProvider、HardCodedNameProvider、WhateverNameProvider 等实现。 (2认同)

ada*_*ncy 9

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

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

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

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


Ste*_*ary 5

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