19

我想为一个名为的类的成员函数创建一个单元测试,该函数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));
        }
    }
}

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

4

9 回答 9

29

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

测试时,将实现换成测试实现。

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

于 2010-07-01T20:19:10.133 回答
19

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

例如,您可以这样做:

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

于 2010-07-01T20:21:17.400 回答
4

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

于 2010-07-01T20:22:16.683 回答
2

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

于 2010-07-01T20:23:38.247 回答
2
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;
    ...
于 2010-07-01T20:26:13.557 回答
1

与其抽象控制台,不如创建一个组件来封装这个逻辑,并测试这个组件,然后在控制台应用程序中使用它。

于 2010-07-01T20:25:14.530 回答
0

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

public void SignInScoreBoard(int steps, string nameOfTopScorer)

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

于 2010-07-15T14:31:31.973 回答
0

几天前我遇到了类似的问题。封装控制台类对我来说似乎有点矫枉过正。基于 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;
    }
}

TextWriter现在,当您向and注入依赖项时,您可以轻松地测试您的实现TextReader(在本例StreamReader中为TextReader

[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.InasTextReaderConsole.Outas TextWriter

IConfirmationProvider cp = new ConfirmationProvider();

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

于 2018-09-19T13:18:26.960 回答
0

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

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

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

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

于 2022-01-12T12:34:32.137 回答