lentinant
lentinant

Reputation: 832

"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.

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.

Upvotes: 6

Views: 8660

Answers (4)

Eva
Eva

Reputation: 449

You can do it much more easier with 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:

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!

Upvotes: 1

Jesus is Lord
Jesus is Lord

Reputation: 15399

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. Also I can just new it up directly in my tests.

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. 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. Then in my web server I depend on the parsed command-line arguments, in this case an 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.

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.).

Upvotes: 0

Jian Huang
Jian Huang

Reputation: 1185

Since you are dealing with environment variables, why don't we wrap the outside dependencies into one EnvironmentHelper class, then inject the dependencies?

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

Upvotes: 1

Nkosi
Nkosi

Reputation: 246998

You need to abstract Environment.GetCommandLineArgs or what ever is eventually calling it behind something you can mock

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

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

}

Upvotes: 11

Related Questions