3

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

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

我能想到的最好的方法是在单元测试中使用 System.Diagnostics.Process 执行应用程序 exe。这行得通。我可以读取输出,一切都很好。

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

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

4

3 回答 3

10

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

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

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

于 2012-05-03T00:19:20.810 回答
1

单元测试不应该需要人工交互。为了使您的应用程序可单元测试,控制台交互应该是抽象的——这可以使用TextReaderTextWriter类轻松完成。你可能会发现这个问题很有帮助。

于 2012-05-03T00:11:32.283 回答
1

There are a lot of answers in the present question, as well as in Best way to unit test console c# app, NUnit Test - Looping - C#, and possibly many others, indicating that direct unit testing of the unmodified, "untestable" console application is not a good way to test. They are all correct.

However, if you really need to test this way for some reason, and if you are able to refer to the console application as a reference from your test project (which, if the two are in the same solution, you likely can), it is possible to do so without resorting to Process.Start. In .NET 4.5 or later, using xunit syntax:

[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);
    }
}

This solution is likely adaptable to .NET 3.5 or later if you handle asynchronous methods differently. AnonymousPipe(Server|Client)Stream was introduced in .NET 3.5. Other unit test frameworks should work with the appropriate syntax changes.

The pipe streams System.IO.Pipes.AnonymousPipeServerStream and System.IO.Pipes.AnonymousPipeClientStream are key to making this solution work. Because a stream has a current position, it does not work as reliably to have two different processes referring to the same MemoryStream at the same time. Using the pipe streams instead allows the streams' use in parent and child processes, as is done here. Running Program.Main(string[]) in a child task is necessary so that the unit test process can read and write from the console while the program is running. The AnonymousPipeClientStream objects should belong to the child task, according to the documentation, which is why they are created within the task runner.

You can obtain exception data from the programTask object if you need to test for exceptions (or, under xunit, use something like Assert.ThrowsAsync<ExpectedException>(Func<Task>) to run the child task).

于 2015-09-11T18:10:22.133 回答