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

时间:2010-07-01 20:15:44

标签: c# visual-studio unit-testing console 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));
        }
    }
}

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

8 个答案:

答案 0 :(得分:23)

您需要将调用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++];
     }
}

进行测试时,请使用测试实现替换实现。

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

答案 1 :(得分:11)

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

例如,你可以这样做:

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方法添加代码以收集输出,以便您可以在测试代码中对其进行断言。

答案 2 :(得分:2)

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

答案 3 :(得分:2)

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

答案 4 :(得分:1)

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

答案 5 :(得分:1)

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 :(得分:0)

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

public void SignInScoreBoard(int steps, string nameOfTopScorer)

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

答案 7 :(得分:0)

几天前,我遇到了类似的问题。封装控制台类对我来说似乎过于矫kill过正。基于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.InTextReader和以Console.OutTextWriter的复制标准方式实现您的实现

IConfirmationProvider cp = new ConfirmationProvider();

仅此而已-一个具有字段初始化功能的附加ctor。