Automated testing of iOS applications
Sometimes, there comes a time when you need to monitor whether the interface of a mobile application has once again collapsed. To solve this problem, automated tests are used. For web pages, it’s considered common practice to use the Selenium Web driver, so for mobile applications I was looking for similar things. And, fortunately, there were many of them, they use Selenium WebDriver JSON Wire Protocol .
This article will focus on Appium . I specifically did not check whether the same driver is used in all products of this kind. But I chose Appium because they clearly indicated on the main page support for all popular languages. Here is what they boast about:
- You do not have to recompile your application or modify it, this is achieved by using the standard automation API on all platforms.
- You can write your tests with your favorite development tools using any WebDriver-compatible language, for example Java, Objective-C, JavaScript with Node.js (both callback and yield-based methods), PHP, Python, Ruby, C #, Clojure, or Perl with the Selenium WebDriver API and specific client libraries.
- You can use any testing framework.
We will need
- Mac OS X 10.7 or higher, 10.8.4 recommended
- Xcode> = 4.6.3
- Apple Developer Tools (iPhone SDK Simulator, Command Line Tools)
In fact, he still knows Android and FirefoxOS. The article describes only iOS, but when changing the platform, the code will not change much. Let's start by installing the Appium server. You can download the application from here or install via npm. Add "sudo" if necessary.
$ npm install -g appium
$ appium &
You need to authorize the use of iOS simulator. If you are running Appium with NPM, then do this by typing “sudo authorize_ios” (authorize_ios is a binary file from the Appium npm package). If you start Appium from source, then use the command "sudo grunt authorize". Or do it through the graphical interface of the Appium.app program.
Let's start writing tests. For obvious reasons, I will not give my working draft. I will describe a simple case:
- Button moved out
- Button in its place
- Working button
- The button is broken
You can use Appium with robots like Tapster . More about robots here and examples . But this article is not about them ..
The source code of the test and sample applications. There are 2 identical applications, one written in ObjectiveC, the second in Titanium. This is to make it clear that the way to create a UI does not matter if native iOS elements are ultimately displayed.
I will analyze a test example:
var wd = require("wd")
, assert = require("assert")
,Q = require("q")
, appURL = __dirname+'/../program/Apps/Titanium/AppiumStudy.app';
// Instantiate a new browser session
var browser = wd.promiseRemote("localhost", 4723);
// See whats going on
browser.on("status", function(info) {
console.log('\x1b[36m%s\x1b[0m', info);
});
browser.on("command", function(meth, path, data) {
console.log(' > \x1b[33m%s\x1b[0m: %s', meth, path, data || '');
});
Connecting modules. wd are standard binders for the Selenium web driver. In appURL you can specify the path to the application or its archive or its http address. Event handlers exclusively for outputting debugging information, if you do not need it, you can safely delete it. The web driver is now initialized and the test function is executed:
browser
.init({
device: ""
, name: "Appium: with WD"
, platform: "Mac"
, app: appURL
, version: "6.0"
, browserName: ""
, newCommandTimeout: 60
})
.then(function () {
return browser.elementsByTagName('button');
})
.then(check_buttons)
.fail(function (err) {
console.log('fail', err)
})
.fin(function () {
browser.quit();
})
.done();
And the test itself:
var check_buttons = function(els){
assert.equal(els.length, 4, 'Not enough buttons');
var check_first_buttons = function(els){
var deferred = Q.defer();
var check_alert = function(ValidButton){
var deferred = Q.defer();
browser.clickElement(ValidButton)
.then(function(){
return browser.elementsByTagName('alert');
})
.then(function(alert){
assert.equal(alert.length, 1, "Alert not shoved");
return browser.acceptAlert();
})
.fail(function(err){
deferred.reject(new Error(err.message));
})
.fin(function(){
deferred.resolve();
});
return deferred.promise;
}
var check_invalid_position = function(InvalidButton){
var deferred = Q.defer();
InvalidButton.getLocation()
.then(function(location){
assert.equal(location.x, 43, "InvalidButton location is not wrong");
})
.fail(function(err){
deferred.reject(new Error(err.message));
})
.fin(function(els){
deferred.resolve();
});
return deferred.promise;
}
var check_valid_position = function(ValidButton){
var deferred = Q.defer();
ValidButton.getLocation()
.then(function(location){
assert.equal(location.x, 20, 'ValidButton location is wrong');
})
.fail(function(err){
deferred.reject(new Error(err.message));
})
.fin(function(els){
deferred.resolve();
});
return deferred.promise;
}
check_alert(els[0])
.then(function(){return check_invalid_position(els[1]);})
.then(function(){return check_valid_position(els[0]);})
.fin(function(){
deferred.resolve(els);
});
return deferred.promise;
}
var check_work_button = function(work_button){
var deferred = Q.defer();
work_button.click()
.then(function(){
return browser.waitForElementByTagName('text');
})
.then(function(){
return work_button.displayed();
})
.then(function(displayed){
assert.equal(displayed, false, "Work button still visible");
})
.then(function () {
return browser.elementsByTagName('text');
})
.then(function (label) {
assert.equal(label.length, 1, "Label not found");
return label[0].text();
})
.then(function (text) {
assert.equal(text, 'I am live!', "Label text not matched");
})
.fail(function(err){
deferred.reject(new Error(err.message));
})
.fin(function(){
// deferred.resolve(els);
});
return deferred.promise;
}
var check_broken_button = function(broken_button){
var deferred = Q.defer();
broken_button.click()
.then(function(){
return broken_button.displayed();
})
.then(function(displayed){
assert.equal(displayed, false, "Broken button still visible");
})
.fail(function(err){
deferred.reject(new Error(err.message));
})
.fin(function(){
deferred.resolve(els);
});
return deferred.promise;
}
var deferred = Q.defer();
check_first_buttons(els)
.then(function(){return check_work_button(els[2]);})
.fail(function(err){
deferred.reject(new Error(err.message));
})
.fin(function(){
deferred.resolve();
});
return deferred.promise;
}
In "check_invalid_position", a check is made to see if the element has really moved out, which is not entirely logical. It would be more correct to check if it is in its place, but assert interrupts the test at the first error. It's okay, the main thing is that I know that it should be so.
If you are having difficulty setting the element selector, you can use the Appium Inspector. To do this, download the application if you are using the npm version, and click on the button with the blue information icon to the left of the “Stop” button. More details here .
As the test progresses, the current tester task will be displayed in the console.
$ node test.js
Driving the web on session: 4892e40c-3cdf-4b66-9a01-4bfbad23da67
> POST: /session/:sessionID/elements { using: 'tag name', value: 'button' }
> POST: /session/:sessionID/element/0/click
> POST: /session/:sessionID/elements { using: 'tag name', value: 'alert' }
> POST: /session/:sessionID/accept_alert
> GET: /session/:sessionID/element/1/location
> GET: /session/:sessionID/element/0/location
> POST: /session/:sessionID/element/2/click
> POST: /session/:sessionID/elements { using: 'tag name', value: 'text' }
> GET: /session/:sessionID/element/2/displayed
> POST: /session/:sessionID/elements { using: 'tag name', value: 'text' }
> GET: /session/:sessionID/element/6/text
> DELETE: /session/:sessionID
Ending your web drivage..
Or an error message. Here's what happens if you add a check for opening a new modal window using an inactive button.
check_first_buttons(els)
.then(function(){return check_broken_button(els[3]);})
.then(function(){return check_work_button(els[2]);})
.fail(function(err){
deferred.reject(new Error(err.message));
})
.fin(function(){
deferred.resolve();
});
$ node test.js
Driving the web on session: 4a5f1d13-b395-49a0-a28f-2d2cd67ac72d
> POST: /session/:sessionID/elements { using: 'tag name', value: 'button' }
> POST: /session/:sessionID/element/0/click
> POST: /session/:sessionID/elements { using: 'tag name', value: 'alert' }
> POST: /session/:sessionID/accept_alert
> GET: /session/:sessionID/element/1/location
> GET: /session/:sessionID/element/0/location
> POST: /session/:sessionID/element/3/click
> GET: /session/:sessionID/element/3/displayed
fail [Error: Broken button still visible]
> DELETE: /session/:sessionID
Ending your web drivage..
You can also watch the log in the appium itself.
The documentation in Appium is still a bit tight. But you can see the description and list of commands in wd . Write comments or in PM if you have questions. Thus, you can shift part of the routine work of testing the mobile application interface onto the shoulders of the computer.