command-line-api icon indicating copy to clipboard operation
command-line-api copied to clipboard

How to test parseArgument

Open SimonCropp opened this issue 2 years ago • 3 comments

So given

var myOptions = new Option<string[]>(
    name: "--my",
    description: "items",
    parseArgument: ParseMyItems);

and

internal static string[] ParseMyItems(ArgumentResult result)
{
    //manipulate/validate/parse items
    return result.Values();
}

I want to test ParseMyItems. so i need to be able to create an instance of ArgumentResult. but the constructor is internal

internal ArgumentResult(
    Argument argument,
    SymbolResult? parent) : base(argument, parent)
{
    Argument = argument;
}

So how do i get an instance of ArgumentResult.

Currently i have this hack:

public static class ArgumentBuilder
{
    static Func<string[], ArgumentResult> construct;

    static ArgumentBuilder()
    {
        var tokensField = typeof(ArgumentResult).GetField("_tokens", BindingFlags.NonPublic | BindingFlags.Instance)!;
        var constructor = typeof(ArgumentResult).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)
            .Single();
        construct = inputs =>
        {
            var invoke = constructor.Invoke(
                new object?[]
                {
                    new Argument<string[]>(_ => inputs),
                    null
                })!;
            var result = (ArgumentResult) invoke;
            var tokens = (List<Token>) tokensField.GetValue(result)!;
            tokens.AddRange(inputs.Select(_ => new Token(_, TokenType.Argument, null!)));
            return result;
        };
    }

    public static ArgumentResult Build(params string[] values) =>
        construct(values);
}

then i can do

var argument = ArgumentBuilder.Build("value1", "value2");
ParseMyItems(argument)

but there must be a better way

SimonCropp avatar Aug 12 '23 00:08 SimonCropp

it would be great if this docs could have code that show how to test AddValidator and parseArgument https://learn.microsoft.com/en-us/dotnet/standard/commandline/model-binding#custom-validation-and-binding

SimonCropp avatar Aug 12 '23 01:08 SimonCropp

You could perhaps wrap the method under test (like ParseMyItems) in its own test method that will assert the MUT's behavior. Basically, (pseudo code!):

void Test()
{
    var myOptions = new Option<string[]>(
        name: "--my",
        description: "items",
        parseArgument: Test_ParseMyItems
    );
    var parseResult = myOptions.Parse(new string[] {"--my", "foo", "bar"});
    
    // Assert parseResult state of interest
    

    static string[] Test_ParseMyItems(ArgumentResult argResult)
    {
        var optionParseResult = ParseMyItems(argResult);
        
        // Assert state of optionParseResult
        
        return optionParseResult;
    }
}

That way, the ArgumentResult is created the way it is supposed to be without you needing to try mimicking the behavior of parts of the library itself.

elgonzo avatar Aug 12 '23 07:08 elgonzo

That way, the ArgumentResult is created the way it is supposed to be without you needing to try mimicking the behavior of parts of the library itself.

I second this approach. Correctly recreating some of these result types in the absence of an actual parse would be fragile. (For example, your reflection code won't work against the code in main today.) We don't test ArgumentResult in isolation in this way even within System.CommandLine's tests. We test exclusively through the public API. So to get the ArgumentResult to pass to the method you want to test, I would recommend parsing some input and getting the ArgumentResult something like this: command.Parse("one two three").GetResult(argument).

jonsequitur avatar Aug 15 '23 19:08 jonsequitur