Friday, March 2, 2012

Automated Testing with MonoTouch on an iPad: Revisited

Back in December, I wrote this post about my efforts to set up automated testing on an iPad in MonoTouch. I updated that a couple of weeks ago, noting that there was now a --launchdev option which might make all of that unnecessary, so long as your test app didn't need any data. Unfortunately, mine did. However, as of MonoTouch 5.2.5 (or potentially earlier, but 5.2.5 is when I noticed it), I saw a new command-line option appear for the mtouch program: --argument. It's description? "Launch the app with this command line argument. This must be specified multiple times for multiple arguments." Could this be a way I could ditch the separate launcher app and related AppleScript incantations completely? The short answer is yes. The long answer follows, after the break.

The --argument command-line option does indeed allow you to pass data into your app programmatically. However, using it is a little counter-intuitive. I had to ask this question on StackOverflow to get it all figured out. This is the basic format of what how you use it:

mtouch --argument=-app-arg:ARGUMENT --launchdev=com.yourcompany.yourapp

This will give you one command-line argument to your app of "ARGUMENT". Now, note that I didn't just say --argument=ARGUMENT - that won't work. You need to use --argument=-app-arg:ARGUMENT, exactly like that. Whatever follows the colon becomes your argument.

Be aware that if you need spaces in your argument value, you'll have to put quotes around it. Also, note that since you're doing this in a shell, parameter substitution will happen within double quotes, but not single quotes. In code, this means:

ARGVAL="my argument value"
mtouch --argument=-app-arg:ARG $ARGVAL --launchdev=com.yourcompany.yourapp
# Your argument list will be { "ARG" }, if mtouch does not error out
mtouch --argument=-app-arg:'ARG $ARGVAL' --launchdev=com.yourcompany.yourapp
# Your argument list will be { "ARG $ARGVAL" }
mtouch --argument=-app-arg:"ARG $ARGVAL" --launchdev=com.yourcompany.yourapp
# Your argument list will be { "ARG my argument value" }

And of course, if you need several arguments, just use the option several times:

mtouch --argument=-app-arg:ARG1 --argument=-app-arg:'ARG2=Some val' --launchdev=com.yourcompany.yourapp
# Your argument list will be { "ARG1", "ARG2=Some val" }

Now, how do you actually use these values in your application? There's two ways that I know of. One, you can do it in your Main method:

namespace MyApp
{
    public class Application
    {
        public static string[] myargs;

        static void Main(string[] args)
        {
            // Here, args is the argument list we've been discussing, but it's hard
            // to do anything besides stick it in a static variable for later.
            myargs = args;

            UIApplication.Main(args, null, "AppDelegate");
        }
    }
}

This isn't the greatest, since I don't have an AppDelegate or a UIViewController yet. But by shoving it in a static variable, I can do this somewhere later on:

foreach (string arg in Application.myargs)
{
    // Do something for each argument
}

Like I said, not the greatest, but it works. But there is a better way. In any context, I can do this:

string[] args = Environment.GetCommandLineArgs();

That gives me the array of arguments pretty much any time I need them. It's a static call, so it's easy to do anywhere. The one thing you have to be aware of is that the arguments start at index 1 with this method, instead of 0 in the previous method. This is because the actual call to launch your application is stored at index 0, and the args come afterwards.

Now, the example used to answer my question on StackOverflow was this:

mtouch --argument=-app-arg:-ip=3.14.15.9 --launchdev=com.yourcompany.yourapp

In this case your argument list will be a single-element array of strings with the single string value being "-ip=3.14.15.9". There's nothing special about the "-ip" or the second '=' there. However, it happens to work very nicely if you do that in conjunction with the new GetOpts-style argument parser (which is in the Mono.NUniteLite assembly). It's pretty handy, and a must-have if you're doing anything complicated with your arguments - or you're just used to similar things in bash, or Python, or C....or Perl....or basically any language you can write command-line utilities with. Here's an example of how you might do that:

using Mono.Options;

namespace MyApp
{
    public partial class MyAppViewController : UIViewController
    {
        protected bool BoolFlag { get; set; }
        protected string IpAddress { get; set; }

        public MyAppViewController(IntPtr handle) : base(handle)
        {
        }

        public override ViewDidLoad()
        {
            // Set up my option parser
            // An individual option consists of a name, a description, and 
            // a delegate that will be called to do something with the value 
            // of the argument, if there is any.
            OptionSet os = new OptionSet() {
                // ip expectes a value, so I put an '=' after the name
                // The lambda expression creates a delegate that sets IpAddress
                // to be the value of the ip option
                { "ip=", "The IP address of the build machine", v => IpAddress = v },
                // boolflag does not expect a value, so no '='
                // The lambda still requires a parameter because of strong 
                // typing, but it is ignored in the delegate body
                { "boolflag", "No value, just a flag", v => BoolFlag = true }
            }

            try
            {
                // Now that our parser is set up, get the arguments from the 
                // environment and parse them.  When an option is found, the 
                // anonymous function we gave it will be called to parse the 
                // associated value.
                os.Parse(Environment.GetCommandLineArgs());
            }
            catch (OptionException oe)
            {
                // An exception is thrown if we can't parse the arguments
                Console.WriteLine(
                    "Got exception {0} parsing option '{1}'", 
                    oe.Message, 
                    oe.OptionName);
            }

            // Other set-up code as needed
        }
    }
}

If some of the above syntax is confusing, check out this post on C# initializers.

No comments:

Post a Comment