简体   繁体   English

单元测试中的“模拟”命令行参数

[英]"Simulate" command line argument in unit-tests

I have some functionality, which depends on command line arguments, and different arguments should lead to different results.我有一些功能,这取决于命令行参数,不同的参数应该导致不同的结果。

I can't directly "simulate" this arguments, since there are some sort of chain dependencies - I need to unit-test some xaml control, which depends on view-model, which depends on certain additional class, which fetches command line arguments using Environment.GetCommandLineArgs , and I can't directly impact on this last class to set arguments manually instead of using GetCommandLineArgs .我不能直接“模拟”这个参数,因为存在某种链依赖关系 - 我需要对一些 xaml 控件进行单元测试,它依赖于视图模型,它依赖于某些额外的类,它使用获取命令行参数Environment.GetCommandLineArgs ,我不能直接影响最后一个类来手动设置参数而不是使用GetCommandLineArgs

So, I'd like to know, is there any way to make Environment.GetCommandLineArgs return value I want it to return, for certain unit-test.所以,我想知道,对于某些单元测试,有什么方法可以让Environment.GetCommandLineArgs返回我希望它返回的值。

You need to abstract Environment.GetCommandLineArgs or what ever is eventually calling it behind something you can mock您需要抽象Environment.GetCommandLineArgs或最终在您可以模拟的东西后面调用它的东西

public interface ICommandLineInterface {
    string[] GetCommandLineArgs();
}

Which can eventually be implemented in a concrete class like最终可以在一个具体的类中实现,比如

public class CommandInterface : ICommandLineInterface {
    public string[] GetCommandLineArgs() {
        return Environment.GetCommandLineArgs();
    }
}

And can be Tested using Moq and FluentAssertions并且可以使用MoqFluentAssertions进行测试

[TestMethod]
public void Test_Should_Simulate_Command_Line_Argument() {
    // Arrange
    string[] expectedArgs = new[] { "Hello", "World", "Fake", "Args" };
    var mockedCLI = new Mock<ICommandLineInterface>();
    mockedCLI.Setup(m => m.GetCommandLineArgs()).Returns(expectedArgs);
    var target = mockedCLI.Object;

    // Act
    var args = target.GetCommandLineArgs();

    // Assert
    args.Should().NotBeNull();
    args.Should().ContainInOrder(expectedArgs);

}

Since you are dealing with environment variables, why don't we wrap the outside dependencies into one EnvironmentHelper class, then inject the dependencies?既然你在处理环境变量,为什么我们不将外部依赖包装到一个 EnvironmentHelper 类中,然后注入依赖呢?

Here is my suggestion:这是我的建议:

public class EnvironmentHelper
{
    Func<string[]> getEnvironmentCommandLineArgs; 

       // other dependency injections can be placed here

       public EnvironmentHelper(Func<string[]> getEnvironmentCommandLineArgs)
       {
            this.getEnvironmentCommandLineArgs = getEnvironmentCommandLineArgs;
       }

       public string[] GetEnvironmentCommandLineArgs()
       {
            return getEnvironmentCommandLineArgs();
       }
}

Here is the Mock method:这是模拟方法:

public static string[] GetFakeEnvironmentCommandLineArgs()
{
    return new string[] { "arg1", "arg2" };
}

In your source code:在您的源代码中:

EnvironmentHelper envHelper = new EnvironmentHelper(Environment.GetCommandLineArgs);
string[] myArgs = envHelper.GetEnvironmentCommandLineArgs();

In your unit test code:在您的单元测试代码中:

EnvironmentHelper envHelper = new EnvironmentHelper(GetFakeEnvironmentCommandLineArgs);
string[] myArgs = envHelper.GetEnvironmentCommandLineArgs();

If you want something unit-testable it should have its dependencies on a abstraction that is at least as strict as its implementation.如果你想要一些可单元测试的东西,它应该依赖于至少与其实现一样严格的抽象。

Usually you'd get the dependencies through your constructor of your class or a property method.通常,您会通过类的构造函数或属性方法获取依赖项。 Constructor is preferred, generally, because now a consumer of your class knows at compile-time what dependencies are needed.通常,构造函数是首选,因为现在您的类的使用者在编译时知道需要哪些依赖项。

public void int Main(string[] args)
{
    // Validate the args are valid (not shown).

    var config = new AppConfig();
    config.Value1 = args[0];
    config.Value2 = int.Parse(args[1]);
    // etc....
}

public class MyService()
{
    private AppConfig _config;

    public MyService(AppConfig config)
    {
        this._config = config;
    }
}

I normally don't put a config object behind an interface because it only has data - which is serializable.我通常不会将配置对象放在接口后面,因为它只有数据——这是可序列化的。 As long as it has no methods, then I shouldn't need to replace it with a subclass with override -d behavior.只要它没有方法,那么我就不需要用具有override -d 行为的子类替换它。 Also I can just new it up directly in my tests.我也可以直接在我的测试中new它。

Also, I've never ran into a situation when I wanted to depend on an abstraction of the command line arguments themselves to a service - why does it need to know it's behind a command-line?此外,我从来没有遇到过这样的情况:我想依赖于对服务本身的命令行参数的抽象——为什么它需要知道它在命令行后面? The closest I've gotten is use PowerArgs for easy parsing, but I'll consume that right in Main .我得到的最接近的是使用PowerArgs来轻松解析,但我会在Main中使用它。 What I normally do is something like maybe read in the port number for a web server on the command-line arguments (I let the user of the app choose so that I can run multiple copies of my web server on the same machine - maybe different versions or so I can run automated tests while I'm debugging and not conflict ports), parse them directly in my Main class.我通常做的可能是在命令行参数上读取 Web 服务器的端口号(我让应用程序的用户选择,以便我可以在同一台机器上运行我的 Web 服务器的多个副本 - 可能不同版本左右我可以在调试时运行自动化测试而不是冲突端口),直接在我的Main类中解析它们。 Then in my web server I depend on the parsed command-line arguments, in this case an int .然后在我的网络服务器中,我依赖于解析的命令行参数,在本例中为int That way the fact that the configuration is coming from a command-line is irrelevant - I can move it to an App.config file later (which is also basically bound to the lifecycle of the process) if I prefer - then I can extract common configuration to configSource files.这样,配置来自命令行的事实是无关紧要的 - 如果我愿意,我可以稍后将其移动到App.config文件(这也基本上与进程的生命周期绑定) - 然后我可以提取公共配置到configSource文件。

Instead of depending on an abstraction for command-line in general (which each service consuming would have to re-parse if you kept it pure), I usually abstract the command-line and App.config dependencies to a strongly-typed object - maybe an app-level config class and a test-level config class and introduce multiple configuration objects as needed - (the app wouldn't necessarily care about this, while the E2E test infrastructure would need this in a separate part of the App.config : where do I grab the client static files from, where do I grab the build scripts in a test or developer environment to auto-generate/auto-update an index.html file, etc.).我通常不依赖于命令行的抽象(如果你保持纯洁,每个服务消耗都必须重新解析),我通常将命令行和App.config依赖项抽象为强类型对象 - 也许一个应用程序级配置类和一个测试级配置类,并根据需要引入多个配置对象 - (应用程序不一定关心这一点,而 E2E 测试基础设施需要在App.config的单独部分中使用它:我从哪里获取客户端静态文件,从哪里获取测试或开发环境中的构建脚本以自动生成/自动更新 index.html 文件等)。

You can do it much more easier with Typemock Isolator .使用Typemock Isolator可以更轻松地完成它。 It allows to mock not only interfaces, so.它不仅允许模拟接口,因此。 Take a look:看一看:

[TestMethod, Isolated]
public void TestFakeArgs()
{
    //Arrange
    Isolate.WhenCalled(() => Environment.GetCommandLineArgs()).WillReturn(new[] { "Your", "Fake", "Args" });

    //Act
    string[] args = Environment.GetCommandLineArgs();

    //Assert
    Assert.AreEqual("Your", args[0]);
    Assert.AreEqual("Fake", args[0]);
    Assert.AreEqual("Args", args[0]);
}

Mocking Environment.GetCommandLineArgs() took only one line: Mocking Environment.GetCommandLineArgs()只用了一行:

Isolate.WhenCalled(() => Environment.GetCommandLineArgs()).WillReturn(new[] { "Your", "Fake", "Args" });

And you don't need to create new Interfaces and to change production code.而且您不需要创建新接口和更改生产代码。

Hope it helps!希望能帮助到你!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM