2011-08-18 78 views
3

我目前正在编写一个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方法中添加很多代码来“伪造”输出,但如果可能的话,我想避免这种情况。如果这是推荐的方式,我可以为自己创建一个脚本来创建所有可能的输出。有另一种方法吗?

回答

8

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

在这种情况下,我们注入ICommandLineRunnerCLIService,我们正在嘲讽调用RunCommand到返回我们事先设置的特定输出(我们的文本文件的内容)。这种方法允许我们测试CLIServiceICommandLineRunner进行了适当的调用,并且它正确地解析了该调用的输出。如果您需要我澄清有关此方法的任何信息,请让我知道。

4

@rsbarro很好地覆盖了他(答案为+1)。就个人而言,我会忘记做集成测试。我的兴趣在于验证我的代码与服务正确交互。我相信你与之交互的服务已经获得了必要的测试,并且我没有理由将这些努力投入到测试中。我的测试应该是我的代码,而不是他们的

+1

要验证码“的服务交互正确”,你的实际需要集成测试(代码和服务必须集成)。但是,除了术语之外,我同意你的回答:你必须测试你的代码(可能与“他们的”代码集成)而不是“他们的”代码。 – igorrs

1

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

主要有两个原因:

  • 提高阶级之间的阶级的设计和交互

几乎没有任何可能,因为你很可能需要一个或改善两个班。此代码根本没有足够的责任。正如你所描述的,一切都适合WCF服务实例。在.NET Process上创建包装是没有意义的,因为它只是一个人造代理。

  • 让你在你的代码有信心,避免错误等

,可以发生在这个代码是,如果你使用.NET进程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之后,微软也会引入另一个标志,这可能会破坏你的代码。这个测试会抓住它吗?否。当您正在运行的EXE将被重命名或更改命令行参数时,您的测试是否会捕获该错误?不,我强烈建议你阅读this文章。这个想法是,这仅仅是一个瘦包装在API你不控制也不需要单位的代码测试,它需要整合测试。但这是一个不同的故事。

我又可能有误解要求,并有更多的给它。也许有一些过程输出的翻译。也许还有其他值得单元测试的东西。对于此代码,也可能仅为educational目的编写单元测试。

P.S.你也提到你有机会从头开始这个项目。如果你决定这个项目不是一个好的候选者,你可以随时开始向旧的代码库引入单元测试。尽管标题是关于单元测试的,但在这个问题上这是一个很好的book

+0

是的,它并不尽如人意,但是这个CLI工具的输出有大量的翻译,这就是我想要测试的。 – Nate