Swapping Selenium::Webdriver for Appium::Driver in Rufus
In the last post I described a method for speeding up the Rufus page object gem by parsing page source data rather than making extra calls to the Selenium::WebDriver. While those changes did speed things up, it just didn’t seem fast enough. Just a few months ago I had seen Frank run through cucumber tests with blazing speed. So I posed the following question to the google group for Appium and was happy to get a quick reply from some of its contributors.
See: Three steps, 38 seconds. Can iOS cucumber tests be faster than this?
I set up a basic test project that used Appium to perform a simple automated test on the iOS simulator. It took 38 seconds to enter text into a textfield and press a button then verify some text showed up. Basically, to get the test project to run faster the Appium group had update me to Mavericks, as well as to the latest Appium revision and to Xcode 5’s latest command line tools. All good stuff I needed to do anyway, especially the Mavericks upgrade. All this allowed me to remove the –native-instruments-lib option that I was passing in when starting the Appium server via Node. So now the test that took 38 seconds to run was down to about 12 seconds.
But I had to get the same performance benefits in Rufus. The key difference between the test project and Rufus was the driver. Rufus used the lower-level Selenium::WebDriver while the test project used the Appium::Driver. Even after performing the upgrades, the Rufus automated test suite did not run without passing in the –native-instruments-lib option. So those tests still were slow. Why is that option so bad? Apple injects a one second delay before any call using instruments gets executed. For Rufus that helped contribute to a test suite of 31 scenarios taking about 24 minutes to complete.
The Appium::Driver, which is a wrapper for Selenium::WebDriver, has found a way around this delay. Not using the –native-instruments-lib option squashes the one second delay. Tests run much faster. The Rufus suite of automated tests now executes all 31 scenarios in a little over seven minutes.
Swapping Selenium::WebDriver for Appium::Driver was no sweat. Rufus is designed to hide the complexity of backend driver implementations by having those implementations use the methods exposed by Rufus::Driver. Selenium::WebDriver and Appium::Driver were so similar that, even at a layer beneath Rufus::Driver, calls that were made to one could be made to the other without changing the method signature.
For instance
selenium.find_element(:name, 'someElement')
worked just like
appium.find_element(:name, 'someElement')
If the config.yml is set to run automated tests on a simulator (i.e. use_physical: false), then the Rufus::DriverFactory will produce an instance of IOS_Simulator. So when we call the method find(:name => ‘someElement’) on this instance of IOS_Simulator, the locator (:name -> ’someElement’) eventually gets parsed and fed into a method which calls selenium.find_element(:name, ‘someElement’). See the implementation of that method below. It is a base method that all Appium/Selenium drivers would use whether running on the simulator or a physical device.
def find(locator)
how = locator.keys[0].to_sym
what = locator[how]
if how.to_s.eql?('label')
locator = generate_name(locator)
find(locator)
else
begin
selenium.find_element(how, what)
rescue Selenium::WebDriver::Error::NoSuchElementError
return nil
end
end
end
In this case notice on line nine the method eventually calls selenium.find_element(:name, ‘someElement’). Now lets examine ‘selenium.’ It is a method that previously returned an instance of Selenium::WebDriver, but now it returns an instance of Appium::Driver. The best part of swapping out one driver for another was that most of the work was done in one method.
It was:
def selenium
@selenium_driver ||= Selenium::WebDriver.for(:remote, :desired_capabilities => capabilities, :url => 'http://127.0.0.1:4723/wd/hub')
end
But now it’s:
def selenium
@selenium_driver ||= Selenium::WebDriver.for(:remote, :desired_capabilities => capabilities, :url => 'http://127.0.0.1:4723/wd/hub')
end
The new selenium method does do a few extra things to get the driver started and we’ll talk about the constructor parameters in a sec, but for the most part this was simply switching one object with another.
The biggest hurdle so far has been in methods where I asked Selenium::WebDriver to get the class type of an element. Before the upgrade I would just call element.tag_name, but now that call throws an error saying that the element does not have an attribute ‘type.’ This Appium::Driver uses a Selenium::WebDriver greater than 2.39.0. This updated version of Selenium::WebDriver doesn’t appear to like using :tag_name as a way to get the class of an element.
This is kinda crazy to me because if you parse the page source data, each element does have an attribute called “type” which has a value for each element. If you had a capable parser, you could get this information. Luckily, Rufus has this capability. We can ask an element for its class using a locator like :name => ‘someElement’ and under the covers it creates an instance of Rufus::Parser, passing in the page source data. The parser can use the page source to find an element based on the locator. In this case it would return all the key-value pairs associated with an element defined by the name ‘someElement’ then it checks for the value of the ‘type’ key. Check out how we find the class of an element then and now.
Then:
def class(locator)
find(locator).tag_name
end
Now:
def class(locator)
view_data = Rufus::Parser.new(page_source).find_view(locator)
view_data["type"]
end
One of the welcome changes between the two drivers was the simplified capabilities definition for the Appium::Driver. Now we can get by with just using a hash that defines the device and the app path whereas the more low-level Selenium::WebDriver needed more.
Old way:
def capabilities
{
'browserName' => iOS,
'platform' => Mac,
'version' => 7.0.3,
'app' => '/path/to/App.app',
'device' => "iPhoneSimulator"
}
end
driver = Selenium::WebDriver.for(:remote, :desired_capabilities => capabilities, :url => 'http://127.0.0.1:4723/wd/hub')
New way:
def app
{device: 'iPad Simulator', app_path: '/path/to/App.app'}
end
appium = Appium::Driver.new(app)
That’s it for now … peace.