【问题标题】:Unit Testing a Console Application inside Visual Studio在 Visual Studio 中对控制台应用程序进行单元测试
【发布时间】:2012-05-12 11:44:30
【问题描述】:

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

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

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

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

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

【问题讨论】:

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


    【解决方案1】:

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

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

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

    【讨论】:

    • 是的。我想多了。这与重定向控制台输出相结合,可以完美运行。非常感谢:)
    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

      目前的问题有很多答案,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>)之类的东西来运行子任务)。

      【讨论】:

        猜你喜欢
        • 2014-03-28
        • 2012-09-18
        • 1970-01-01
        • 2014-11-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-13
        相关资源
        最近更新 更多