【问题标题】:Unit Testing a Service fully dependent on third-party application对完全依赖于第三方应用程序的服务进行单元测试
【发布时间】:2011-08-18 21:14:50
【问题描述】:

我目前正在用 C# 编写一个 Windows 服务,该服务将负责托管 WCF 服务。当调用服务操作时,我的服务将执行一个命令行应用程序,对 StandardOutput 进行一些数据处理并返回结果。此命令行应用程序更改服务器上其他第三方服务的状态。

这是我真正能够从头开始的罕见情况,所以我想以一种可以轻松进行单元测试的方式正确设置它。没有遗留代码,所以我有一张白纸。我正在苦苦挣扎的是如何使我的服务可单元测试,因为它几乎完全依赖于外部应用程序。

我的服务操作与命令行应用程序执行的操作的比例大致为 1:1。如果您没有猜到,我正在构建一个工具来允许远程管理仅提供 CLI 管理的服务。

这只是单元测试麻烦多于其价值的情况吗?为了提供一些背景信息,下面是我的概念验证应用程序中的一个快速示例。

    private string RunAdmin(String arguments)
    {
        try
        {
            var exe = String.Empty;
            if (System.IO.File.Exists(@"C:\Program Files (x86)\App\admin.exe"))
            {
                exe = @"C:\Program Files (x86)\App\admin.exe";
            }
            else
            {
                exe= @"C:\Program Files\App\admin.exe";
            }

            var psi = new System.Diagnostics.ProcessStartInfo
            {
                FileName = exe,
                UseShellExecute = false,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
            };

            psi.Arguments = String.Format("{0} -u {1} -p {2} -y", arguments, USR, PWD);

            var adm= System.Diagnostics.Process.Start(psi);

            var output = fmsadmin.StandardOutput.ReadToEnd();

            return output;
        }
        catch (Exception ex)
        {
            var pth = 
                System.IO.Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
                    "FMSRunError.txt");

            System.IO.File.WriteAllText(pth, String.Format("Message:{0}\r\n\r\nStackTrace:{1}", ex.Message, ex.StackTrace));

            return String.Empty;
        }
    }

    public String Restart()
    {
        var output = this.RunAdmin("/restart");
        return output; // has cli tool return code
    }

如果不向我的 RunAdmin 方法添加大量“测试”代码,有没有办法对此进行单元测试?显然,我可以在 RunAdmin 方法中添加大量代码来“伪造”输出,但如果可能的话,我想避免这种情况。如果这是推荐的方式,我可能可以为自己创建一个脚本来创建所有可能的输出。还有其他方法吗?

【问题讨论】:

    标签: c# .net unit-testing


    【解决方案1】:

    IMO,想出一种方法来测试您正在编写的代码不会比它的价值更麻烦。拥有良好的测试可以更轻松地让新功能发挥作用、修复错误和维护您的应用程序。根据您的描述,我认为您需要决定哪些测试更适合您的情况:单元测试或集成测试。

    如果您想编写真正的单元测试,那么您将不得不抽象出命令行工具,这样您就可以针对一个始终为您的请求返回相同预期响应的实体编写测试。如果您创建一个接口来包装对命令行工具的调用,那么您可以轻松地模拟响应。如果您使用这种方法,那么重要的是要记住您正在测试的是您的服务按预期响应命令行工具的输出。您必须伪造命令行工具可能发出的所有潜在响应。显然,如果输出非常复杂,这可能不是一个选择。如果你认为你可以伪造响应,那么看看那里的一些模拟框架(我喜欢Moq)。他们会让工作变得更容易。

    另一种方法是使用集成测试。这些测试将依赖于您的服务运行命令行工具并检查您的服务返回的真实响应。以这种方式进行测试很可能需要您有办法将您正在测试的机器重置回其原始状态,假设命令行应用程序实际上会对该机器进行更改。根据我的经验,这些测试通常会运行得更慢并且更难维护。但是,如果从命令行工具返回的数据太难以伪造,那么集成测试是一种可行的方法。

    另一种方法,也可能是我会采用的方法,是两者都用。当命令行工具很容易伪造时,使用单元测试。如果不是,请使用集成测试。对于我的项目,当我可以编写不依赖任何外部的真正单元测试时,我非常喜欢。不幸的是,由于我必须处理很多旧代码,这并不总是可能的。然而,即使是这样,如果我可以对整个事情进行高级集成测试,我总是会感觉更好,只是为了增加一点覆盖率。例如,如果我正在编写一个网站,我可能有 100 个单元测试,涵盖了构建网页的所有细节,但如果可以,我将有 1 或 2 个集成测试为了理智起见,请求并检查页面上的文本。进行这两种类型的测试无疑让我对代码在上线时能够按预期工作更有信心。

    希望对您有所帮助。

    编辑
    我在考虑这个问题,并且可能有一种相对简单的方法来处理您的应用程序的单元测试。要伪造来自命令行工具的输入,只需针对您要测试的任何场景运行命令行工具。将输出保存为文本文件,然后将该文本文件用作测试的输入。如果您使用接口和依赖注入,则无论您是运行测试还是实际应用程序,您的服务代码都是相同的。例如,假设我们正在测试一个打印 CLI 版本号的方法:

    public interface ICommandLineRunner
    {
        string RunCommand(string command);
    }
    
    public class CLIService
    {
       private readonly ICommandLineRunner _cliRunner;
       public CLIService(ICommandLineRunner cliRunner)
       {
           _cliRunner = cliRunner;
       }
    
       public string GetVersionNumber()
       {
           string output = _cliRunner.RunCommand("-version");
           //Parse output and store in result
           return result;        
       }
    }
    
    [Test]
    public void Test_Gets_Version_Number()
    {
        var mockCLI = new Mock<ICommandLineRunner>();
        mockCLI.Setup(a => a.RunCommand(It.Is<string>(s => s == "-version"))
           .Returns(File.ReadAllText("version-number-output.txt"));
    
        var mySvc = new CLIService(mockCLI.Object);
        var result = mySvc.GetVersionNumber();
        Assert.AreEqual("1.0", result);
    }
    

    在这种情况下,我们将ICommandLineRunner 注入CLIService,并模拟对RunCommand 的调用以返回我们事先设置的特定输出(文本文件的内容)。这种方法允许我们测试CLIServiceICommandLineRunner 的正确调用,以及它是否正确解析了该调用的输出。如果您需要我澄清有关此方法的任何内容,请告诉我。

    【讨论】:

      【解决方案2】:

      @rsbarro 很好地涵盖了它(+1 对他的回答)。就个人而言,我会忘记进行集成测试。我的兴趣是验证我的代码是否与服务正确交互。我相信您与之交互的服务已经接受了必要的测试,并且我没有理由投入精力进行测试。我的测试应该是关于我的代码,而不是他们的

      【讨论】:

      • 要验证代码“与服务正确交互”,您实际上需要集成测试(代码和服务必须集成)。但是,除了术语,我同意你的回答:你必须测试你的代码(可能与“他们的”代码集成)而不是“他们的”代码。
      【解决方案3】:

      只是为您提供第二个意见。根据您的描述和代码示例,我很难找到不依赖于系统调用的代码。此代码中的唯一逻辑是“调用 .NET Process 类”。它实际上是System.Diagnostics.Process 上的一对一网络包装器。流程输出按原样返回到 Web 客户端,没有翻译。

      编写单元测试的两个主要原因:

      • 改进类的设计和类之间的交互

      几乎没有什么可以改进的,因为您几乎需要一两个课程。这段代码根本没有足够的责任。正如您所描述的,一切都适合 WCF 服务实例。在 .NET Process 上创建包装器是没有意义的,因为它只是一个人工代理。

      • 让您对自己的代码充满信心,避免出现错误等

      此代码可能出现的唯一错误是您错误地使用了 .NET Process API。由于您不控制此 API,因此您的测试代码只会重申您对 API 的假设。让我举一个例子。假设您或您的同事不小心设置了RedirectStandardOutput = false。这将是您的代码中的一个错误,因为您将无法获得进程输出。你的测试会抓住它吗?我不这么认为,除非你真的会写这样的东西:

      public class ProcessStartInfoCreator {
          public static ProcessStartInfo Create() {
              return new ProcessStartInfo {
                  FileName = "some.exe",
                  UseShellExecute = false,
                  RedirectStandardInput = true,
                  RedirectStandardOutput = false  // <--- BUG
              };
          }
      }
      
      [Test]
      public void TestThatFindsBug() {
          var psi = ProcessStartInfoCreator.Create();
          Assert.That(psi.RedirectStandardOutput, Is.True);
      }
      

      您的测试只是重复您的代码,您对 API 的假设并不属于您。即使在您浪费时间引入像 ProcessStartInfoCreator 这样的人工、不需要的类之后,Microsoft 也会引入另一个可能会破坏您的代码的标志。这个测试会抓住它吗?否。当您运行的 EXE 将被重命名或更改命令行参数时,您的测试会捕获错误吗?不,我强烈建议您阅读this 文章。这个想法是,代码只是您无法控制的 API 的薄包装器,不需要单元测试,它需要 集成 测试。但这是另一回事。

      我可能再次误解了要求,而且还有更多。也许有一些过程输出的翻译。也许还有其他值得单元测试的东西。出于educational 的目的,也可能值得为此代码编写单元测试。

      附:你还提到你有机会从头开始这个项目。如果你认为这个项目不是一个好的候选者,你总是可以开始将单元测试引入遗留代码库。这是一个非常好的 book 在这个主题上,尽管它的标题是关于单元测试的。

      【讨论】:

      • 是的,不是很清楚,但是这个 CLI 工具的输出有大量的翻译,这就是我想要测试的。
      猜你喜欢
      • 1970-01-01
      • 2013-09-26
      • 2019-03-21
      • 2019-04-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-25
      相关资源
      最近更新 更多