单元在Visual Studio中测试控制台应用程序

时间:2012-05-02 23:47:50

标签: visual-studio-2010 unit-testing console-application

我在Visual Studio中有一个测试项目我想用(在同一个解决方案中)测试我的控制台应用程序。

我正在尝试设置使用特定参数调用控制台应用程序的测试,并将实际输出与我期望的结果进行比较,然后执行我常用的Assert语句以适当地通过/不通过测试。

我能想到的最好的方法是在单元测试中使用System.Diagnostics.Process执行app exe。这有效。我可以阅读输出,一切都很好。

我遇到的问题是当我想在控制台应用程序代码中设置断点时,我可以做一些调试。由于Process启动了控制台应用程序,因此Visual Studio不会监视控制台应用程序,因此它不会中断。没有什么比“等待来自外部应用程序的请求”更像Web应用程序,我理解为什么,但这基本上就是我正在寻找的。

所以我的问题是,有没有办法在Visual Studio中设置这些单元测试,在那里我仍然可以调试控制台应用程序?我能想到的唯一解决方法是在控制台应用程序上设置Start Action以启动一个外部程序,该程序将调用MSTest.exe,并以这种方式运行相应的单元测试。但这似乎是一个问题,我只是想错了,实际上有一个很多更明显的解决方案。

3 个答案:

答案 0 :(得分:10)

使您的控制台应用程序尽可能精简,并将所有业务逻辑移至域类。 E.g。

class Program
{
    static void Main(string[] args)
    {
       Foo foo = new Foo(args);
    }
}

之后,您可以轻松地为您的Foo类编写单元测试。

答案 1 :(得分:1)

单元测试不应该需要人工干预。要使您的应用程序可单元测试,应该抽象控制台交互 - 这可以使用TextReaderTextWriter类轻松完成。您可能会发现this question有帮助。

答案 2 :(得分:1)

本问题以及Best way to unit test console c# appNUnit Test - Looping - C#以及可能还有许多其他问题都有很多答案,表明未经修改的“不可测试”控制台应用程序的直接单元测试是不是一个好的测试方法。他们都是正确的。

但是,如果您确实需要出于某种原因以这种方式进行测试,并且如果您能够将控制台应用程序作为测试项目的参考(如果两者在同一解决方案中,您可能会),可以在不诉诸Process.Start的情况下这样做。在.NET 4.5或更高版本中,使用xunit语法:

[Theory]
[MemberData("YourStaticDataProviderField")]
public async void SomeTest(string initialString, string resultString, params int[] indexes)
{
    using (var consoleInStream = new AnonymousPipeServerStream(PipeDirection.Out))
    using (var consoleOutStream = new AnonymousPipeServerStream(PipeDirection.In))
    using (var writer = new StreamWriter(consoleInStream, Encoding.Default, 1024, true))
    using (var reader = new StreamReader(consoleOutStream, Encoding.Default, false, 1024, true))
    using (var tokenSource = new CancellationTokenSource())
    {
        // AutoFlush must be set to true to emulate actual console behavior,
        // else calls to Console.In.Read*() may hang waiting for input.
        writer.AutoFlush = true;

        Task programTask = Task.Run(() =>
        {
            using (var consoleInReader =
                new StreamReader(new AnonymousPipeClientStream(PipeDirection.In,
                                                               consoleInStream.GetClientHandleAsString())))
            using (var consoleOutWriter =
                new StreamWriter(new AnonymousPipeClientStream(PipeDirection.Out,
                                                               consoleOutStream.GetClientHandleAsString())))
            {
                // Again, AutoFlush must be true
                consoleOutWriter.AutoFlush = true;
                Console.SetIn(consoleInReader);
                Console.SetOut(consoleOutWriter);
                // Of course, pass any arguments your console application
                // needs to run your test.  Assuming no arguments are
                // needed:
                Program.Main(new string[0]);
            }
        }, tokenSource.Token);

        // Read and write as your test dictates.
        await writer.WriteLineAsync(initialString.Length.ToString());
        await writer.WriteLineAsync(initialString);
        await writer.WriteLineAsync(indexes.Length.ToString());
        await writer.WriteLineAsync(String.Join(" ", indexes));

        var result = await reader.ReadLineAsync();

        await writer.WriteLineAsync();

        // It is probably a good idea to set a timeout in case
        // the method under test does not behave as expected (e.g.,
        // is still waiting for input).  Adjust 5000 milliseconds
        // to your liking.
        if (!programTask.Wait(5000, tokenSource.Token))
        {
            tokenSource.Cancel();
            Assert.False(true, "programTask did not complete");
        }

        // Assert whatever your test requires.
        Assert.Null(programTask.Exception);
        Assert.Equal(resultString, result);
    }
}

如果以不同方式处理异步方法,此解决方案可能适用于.NET 3.5或更高版本。 AnonymousPipe(Server|Client)Stream是 在.NET 3.5中引入。其他单元测试框架应该与 适当的语法更改。

管道流System.IO.Pipes.AnonymousPipeServerStreamSystem.IO.Pipes.AnonymousPipeClientStream是使此解决方案有效的关键。因为流具有当前位置,所以它不能可靠地使两个不同的进程同时引用相同的MemoryStream。使用管道流允许流在父进程和子进程中使用,如此处所做的那样。在子任务中运行Program.Main(string[])是必要的,这样单元测试过程可以在程序运行时从控制台读取和写入。根据文档,AnonymousPipeClientStream对象应该属于子任务,这就是它们在任务运行器中创建的原因。

如果需要测试异常,可以从programTask对象获取异常数据(或者,在xunit下,使用Assert.ThrowsAsync<ExpectedException>(Func<Task>)之类的东西来运行子任务)。