单元测试服务完全依赖于第三方应用程序

时间:2011-08-18 21:14:51

标签: c# .net unit-testing

我目前正在C#中编写一个Windows服务,负责托管WCF服务。调用Service Operations时,我的服务将执行命令行应用程序,对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方法中添加大量代码来“伪造”输出,但我想尽可能避免这种情况。如果这是推荐的方式,我可以为自己创建一个脚本来创建所有可能的输出。还有另一种方式吗?

3 个答案:

答案 0 :(得分: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);
}

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

答案 1 :(得分:4)

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

答案 2 :(得分:1)

只是为你提供第二意见。根据您的描述和代码示例,我很难找到不依赖于系统调用的代码。这段代码中唯一的逻辑是“调用.NET Process class”。它实际上是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目的而编写此代码的单元测试也是值得的。

P.S。您还提到您有机会从头开始使用此项目。如果您认为该项目不是一个好的候选项,您可以随时开始将单元测试引入遗留代码库。这是一个非常好的book关于这个主题,尽管标题是单元测试。