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

C# 单元测试完全依赖于第三方应用程序的服务,c#,.net,unit-testing,C#,.net,Unit Testing,我目前正在用C#编写一个Windows服务,负责托管WCF服务。调用服务操作时,我的服务将执行命令行应用程序,对StandardOutput执行一些数据处理并返回结果。此命令行应用程序更改服务器上其他第三方服务的状态 这是一个罕见的例子,我真的能够从头开始,所以我想正确地设置它,并以一种可以轻松进行单元测试的方式。没有遗留的代码,所以我没有保留。我正在努力解决的是如何使我的服务单元可测试,因为它几乎完全依赖于外部应用程序 我的服务操作与命令行应用程序执行的操作的比例约为1:1。如果您还没有猜到,

我目前正在用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方法添加大量代码以“伪造”输出,但如果可能的话,我希望避免这种情况。如果这是推荐的方法,我可能可以为自己创建一个脚本来创建所有可能的输出。还有其他方法吗?

在我看来,想出一种方法来测试您正在编写的代码不会太麻烦。通过良好的测试,可以更容易地使用新功能、修复bug和维护应用程序。根据您所描述的内容,我认为您需要决定在您的案例中使用哪些测试更好:单元测试还是集成测试

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

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

另一种方法,也可能是我会采用的方法,是两者兼用。当命令行工具很容易伪造时,使用单元测试。如果不是,则使用集成测试。对于我的项目,我非常喜欢编写真正的单元测试,而不依赖于任何外部测试。不幸的是,由于我必须处理很多旧代码,这并不总是可能的。然而,即使是这样,如果我能在整个过程中进行一次高级集成测试,仅仅为了增加一点覆盖率,我也会感觉更好。例如,如果我在写一个网站,我可能会有100个单元测试来覆盖构建网页的所有细节,但是如果可以,我会有1到2个集成测试来执行一个web请求并检查页面上的文本,这只是出于理智的考虑。拥有这两种类型的测试肯定会给我更高的信心,让我相信代码在上线时会像预期的那样工作

希望有帮助

编辑
我在进一步考虑这个问题,可能有一种相对简单的方法来处理应用程序的单元测试。要伪造来自命令行工具的输入,只需针对您尝试测试的任何场景运行命令行工具。将输出另存为文本文件,然后将该文本文件用作测试的输入。如果使用接口和依赖项注入,则无论是运行测试还是实际应用程序,您的服务代码都是相同的。例如,假设我们正在测试一个将打印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
{
专用只读ICommandLineRunner\u cliRunner;
公共CLIService(ICommandLineRunner cliRunner)
{
_cliRunner=cliRunner;
}
公共字符串GetVersionNumber()
{
字符串输出=_cliRunner.RunCommand(“-version”);
//解析输出并存储结果
返回结果;
}
}
[测试]
公共无效测试获取版本号()
{
var mockCLI=new Mock();
mockCLI.Setup(a=>a.RunCommand(It.Is(s=>s==“-version”))
.Returns(File.ReadAllText(“version number output.txt”);
var mySvc=新的CLIService(mockCLI.Object);
var result=mySvc.GetVersionNumber();
断言.AreEqual(“1.0”,结果);
}
在本例中,我们将
ICommandLineRunner
注入
CLIService
,模拟调用
RunCommand
,以返回预先设置的特定输出(文本文件的内容)。这种方法允许我们测试
CLIService
是否正确调用
IComma
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);
}