单元测试中查看控制台输出

4

有没有办法在我的抽象类Question的单元测试中检查控制台的输出?

我正在使用NUnit和Moq。

我的单元测试看起来像这样:

    [Test]
    public void QuestionAsk()
    {
        var mock = new Mock<Question>(new object[]{"question text",true});

        mock.CallBase = true;

        var Question = mock.Object;

        Question.Ask();

        mock.Verify(m => m.Ask(), Times.Exactly(1));

    }

在这里,我正在检查是否调用了Question.Ask(),并且它运行良好。Ask()不返回任何值,因此我无法将其赋值给变量。该函数只是输出到控制台。

有没有办法在测试中验证输出是否等于“问题文本”?

编辑:忘记提到Question是一个抽象基类。

我尝试了建议使用的Concole.Setout方法,代码如下:

    [Test]
    public void QuestionAsk()
    {
        var mock = new Mock<Question>(new object[]{"question text",true});

        mock.CallBase = true;

        var Question = mock.Object;

        using (var consoleText = new StringWriter())
        {
            Console.SetOut(consoleText);
            Question.Ask();
            Assert.That(consoleText.ToString(), Is.StringMatching("question text"));
        }
        mock.Verify(m => m.Ask(), Times.Exactly(1));

    }

但是测试用了236毫秒,这对于一个测试来说太长了。实现IWriter接口似乎是处理它的最佳方法,所以我现在会尝试一下。


1
请查看此链接: https://dev59.com/52025IYBdhLWcg3wsIIU。 - Keith Payne
除了 @Keith 的链接之外,您实际上不希望从您的“业务对象”中调用控制台。执行输出是运行程序的任务,因为您现在不能将此类用于 Web 应用程序,例如。如果您只是让它返回字符串,则调用程序可以决定如何输出它。 - CodeCaster
@CodeCaster 说得好。我最终会将其移植到Web上,所以我应该尽早处理这个问题。我正在做这个项目来学习C#。非常感谢你的建议。 - Guerrilla
2个回答

5
您可以使用自定义输出写入器初始化Question,然后模拟编写器以验证输出:
public interface IOutputWriter 
{
    void WriteLine(string s);
}

// Use this console writer for your live code
public class ConsoleOutputWriter : IOutputWriter
{
    public void WriteLine(string s)
    {
        Console.WriteLine(s);
    }
}

public abstract class Question
{
    private readonly IOutputWriter _writer;
    private readonly string _text;
    private readonly bool _default;

    public Question(IOutputWriter writer, params object[] args)
    {
        _writer = writer;
        _text = (string)args[0];
        _default = (bool)args[1];
    }

    public void Ask()
    {
        _writer.WriteLine(_text);
    }
}


[Test]
public void QuestionAsk()
    {
        var writer = new Mock<IOutputWriter>();

        var mock = new Mock<Question>(writer.Object, new object[]{"question text",true});

        mock.CallBase = true;

        var Question = mock.Object;

        Question.Ask();

        mock.Verify(m => m.Ask(), Times.Exactly(1));
        mock.Verify(w => w.WriteLine(It.Is<string>(s => s == "question text")), Times.Once)

    }

谢谢,这似乎是最好的方法。我最初喜欢Console.SetOut方法,因为它只需要一行代码就可以添加到我的现有测试中,但它使我的测试时间从6毫秒增加到了236毫秒。这会让我陷入困境,因为我计划进行大量测试。使用SetOut会导致性能下降吗?还是我做错了什么?我可以将我的代码粘贴在答案中吗?还是我需要提一个新问题?抱歉,我还是新手。 - Guerrilla
你可以对自己的问题发布答案。 - Keith Payne
我怀疑最后一行 mock.Verify(w => w.WriteLine(It.Is<string>(s => s == "question text")), Times.Once) 应该改为 writer.Verify(w => w.WriteLine(It.Is<string>(s => s == "question text")), Times.Once)。另一个问题是,Question 类中的 Ask 应该是 virtual 的。 - Daryn

3
你的测试看起来很奇怪 - 你正在测试模拟对象而不是测试应用程序将使用的真实对象。如果你正在测试“Question”对象,那么你应该使用完全相同类型的“Question”实例,就像你的应用程序会使用的一样。需要模拟的是“Question”的依赖项。因为类应该在隔离中进行单元测试(否则对“Question”的依赖问题将导致测试失败,而它本来是正常工作的)。
所以,如果你有一个显示内容在控制台上的“Question”对象(例如它依赖于“Console”),那么单元测试就需要模拟这个依赖关系。你不能使用Moq来模拟“Console”,因为它是静态类。因此,你应该为控制台创建抽象,该抽象将被“Question”使用:
public interface IConsole
{
    void Write(string message);
}

现在将这个依赖项注入到你的Question中:
public class Question
{
    private IConsole _console;
    private string _message;

    public class Question(IConsole console, string message)
    {
        _console = console;
    }
}

用这段代码,您可以编写 Question 行为的测试:
[Test]
public void ShouldAskQuestionOnConsole()
{
    var message = "Hello World";
    var consoleMock = new Mock<IConsole>();
    consoleMock.Setup(c => c.Write(message));
    var question = new Question(consoleMock.Object, message);

    question.Ask();     

    consoleMock.VerifyAll();
}

这个测试规定,当执行Ask时,问题应该将其消息发送到控制台。实现很简单:

public void Ask()
{
    _console.Write(_message);
}

现在您有一个可用的Question。您需要实现IConsole,这将由您的应用程序使用。它是一个简单的包装器:

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

在您的实际应用程序中,将此实现注入到问题中(这可以通过依赖注入框架自动完成):
IConsole console = new ConsoleWrapper();
Question question = new Question(console, message);

注意:我会选择一些接口,例如 IView,而非 IConsole。这样可以完全将 Question 类从 UI 的类型中抽象出来。第二个注意点是将业务逻辑与表现逻辑分离。通常情况下不会有一个类负责两件事情 —— 维护问题的数据(及可能对其进行处理),以及与用户交互。通常会有类似于控制器的东西,它会接收用户输入,刷新 UI 并请求业务模型的数据。

抱歉,我应该明确一下,Question是一个抽象类。 - Guerrilla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接