Thursday July 14, 2011

✦ UI Automation Part 2: Assertions and Imports

Looking for more information about UI Automation? Check out my new book, published through the Pragmatic Programmers. Thanks for your support!

This is part of a larger study about UI Automation. For a good collection of the resources I’ve worked on, check out the features page.

When we last left off in part 1, we learned how to search for elements in an iOS app’s view and interact with them. We learned a bit about how to use logElementTree() on any UIAElement to figure out a path to what’s inside. We set up wip.js, our “work in progress” file we can use as a playground to explore the Core Data Books sample app. If you haven’t gone through the first part, you should.

Now, let’s start asserting that our interface works the way we expect.

Logging

We need a way for our tests to talk back to us. The singleton UIALogger gives us the basic logMessage() method that does pretty much what you’d expect.

// Dig down to the nav bar

var target = UIATarget.localTarget();
var app = target.frontMostApp();
var window = app.mainWindow();
var navBar = window.navigationBar();
var navBarLabel = navBar.staticTexts()[0];

// Tell us what title you see in the navigation bar

UIALogger.logMessage("The navigation bar title is " + navBarLabel.name());

Put this in your wip.js file, run it against the Core Data Books application, and you’ll see the message in the ugly log pane.1 At the very least, you could use this to output error messages if elements on the screen aren’t what you expect, like so:

pipe this output unix-style into other tools we build ourselves.

// ....

var realTitle = navBarLabel.name();

if (realTitle != "Other Title") {
  UIALogger.logMessage("Expected 'Other Title' but was '" + realTitle + "'!");
}

// ....

Apple provides a nice mechanism to help group tests together by calling UIALogger.logStart(message). Now, all log output will be nested inside this parent group until you close it. If you decide that this chunk of tests pass, you close this group with UIALogger.logPass(message). If you determine that there was an error, you can use UIALogger.logFail(message).

The these log groups can be collapsed in the log pane. The final state (pass or fail) of the group will show up in that collapsed row and you can expand the row to see the details if you need to.

// Start the log "group" with this message
UIALogger.logStart("Checking the navigation bar title");

// A flag we'll use to know if log group passed
var hasError = false;

var realTitle = navBarLabel.name();

if (realTitle != "Other Title") {
  // When the assertion above fails, the following
  // error is output in red within the log group.
  UIALogger.logError("Expected 'Other Title' but was '" + realTitle + "'!");

  // Set the error flag so we know how to close this log group
  hasError = true;
}

// Now, we check to see if any of the tests above had failed.
// Calling logFail() displays the test group in red.
if (hasError) {
  UIALogger.logFail("Some tests failed");
} else {
  UIALogger.logPass("Tests passed");
}

Grouped Test

We Need Help

This is a very low level way to work. It would be better to have an abstraction on top that simplifies our test cases and assertions. You could use something like Jasmine and add your own matchers and such that relate to UI Automation. But in this case, I think that’s a bit overkill. I like to use frameworks as lightweight as I can get away with. I found Tuneup JS by Alex Vollmer to do just fine.

Tuneup JS was built with UI Automation in mind. Its codebase is easy to understand and extend, even for a beginner. Clone the Tuneup JS repo into your ui_testing folder and put it under lib so your structure looks like this:

ui_testing
  \--lib
      \--tuneup_js
          |--assertions.js
          |--lang-ext.js
          |--screen.js
          |--test.js
          |--tuneup.js
          \--uiautomation-ext.js

The UI Automation scripting environment lets you import other javascript files relative to the currently executing file. Put this at the top of wip.js:

#import "lib/tuneup_js/tuneup.js"

You now have access to all the goodies in your script file. Let’s rewrite our test above with this framework.

test("Checking the navigation bar label", function(target, app) {
  assertEquals(navBarLabel.name(), "Books");
});

Simplified Test Output

Put as many assertions as necessary in there. If any of them fail, the whole test group will fail and Tuneup even gives you a dumped element tree of the entire window at the point of failure. Try altering the test above so it fails on purpose and you can see what the messages look like.

For the Javascript newbies, you’re calling Tuneup’s global test() function with two parameters: a string naming the test, and an anonymous function to be called inside test() that actually does the work. Notice that this anonymous function takes two optional parameters, target and app. Tuneup passes UIATarget.localTarget() and it’s application in so you don’t have to keep fetching them yourself. You can leave those two parameters out if you don’t happen to need them. All arguments are optional in Javascript. For completeness, here’s how we could structure the whole wip.js file if we used the parameters handed in.

#import "lib/tuneup_js/tuneup.js"

test("Checking the navigation bar label", function(target, app) {
  // An example using the `app` parameter provided for us by Tuneup
  var window = app.mainWindow();
  var navBar = window.navigationBar();
  var navBarLabel = navBar.staticTexts()[0];

  assertEquals(navBarLabel.name(), "Books");
});

Inside the anonymous function, we make our assertions. assertEquals() is one of the many global functions Tuneup provides in assertions.js. It raises an exception if the first parameter doesn’t equal the second parameter. That exception is caught by the enclosing test() function which balances the UIALogger.logStart(...) and UIALogger.logFail/Pass(...) calls for you.

Magic!

Tune in next time…

At this point, you have the basics for manipulating and querying a live iOS app’s interface. And we have a nice test framework to help us organize and output our assertions. When I’m able to continue this series we’ll start building a library of objects that know how to interact with our screens.

This is part of a larger study about UI Automation. For a good collection of the resources I’ve worked on, check out the features page.

  1. Yes, it is ugly. Please, Apple, please give us a means to