Pull to refresh

Автоматическое тестирование iOS приложений

Reading time 6 min
Views 30K
image
Бывает, наступает момент, когда нужно следить, не развалился ли лишний раз интерфейс мобильного приложения. Чтобы решить эту проблему используются автоматические тесты. Для веб страниц считается общепринятой практикой использовать Selenium Web driver, поэтому для мобильных приложений я искал похожие вещи. И, на счастье, таких нашлось немало, в них используется Selenium WebDriver JSON Wire Protocol.

В этой статье речь пойдет о Appium. Я специально не проверял, один ли и тот же драйвер используется во всех продуктах такого рода. Но Appium я выбрал из-за того, что они четко указали на главной странице поддержку всех популярных языков. Вот то, чем они хвалятся:
  1. Вы не должны перекомпилировать свое приложение или изменять его, это достигается за счет использования стандартного API автоматизации на всех платформах.
  2. Вы можете писать свои тесты своими любимыми средствами разработки, используя любой WebDriver-совместимый язык, например Java, Objective-C, JavaScript с Node.js (как callback так и yield-based способы), PHP, Python, Ruby, C#, Clojure, или Perl с Selenium WebDriver API и специфическими клиентскими библиотеками.
  3. Вы можете использовать любой тестирующий фреймворк.

Нам потребуется
  • Mac OS X 10.7 или выше, рекомендовано 10.8.4
  • XCode >= 4.6.3
  • Apple Developer Tools (iPhone симулятор SDK, инструменты командной строки)

На самом деле он еще умеет Android и FirefoxOS. В статье описывается только iOS, но при смене платформы код сильно не поменяется. Начнем с установки Appium сервера. Можно скачать приложение отсюда или установить через npm. Добавьте «sudo», если потребуется.

$ npm install -g appium
$ appium &

Вам нужно авторизировать использование iOS симулятора. Если вы запускаете Appium с NPM, тогда сделайте это введя «sudo authorize_ios» (authorize_ios это бинарный файл с Appium npm пакета). Если же вы запускаете Appium с исходников, тогда используйте команду " sudo grunt authorize". Или сделайте это через графический интерфейс программы Appium.app.

Приступим к написанию тестов. Свой рабочий проект я, с понятных причин, приводить не буду. Опишу простой случай:
  1. Кнопка съехала
  2. Кнопка на своем месте
  3. Кнопка рабочая
  4. Кнопка битая

Вы можете использовать Appium с роботами, например, Tapster. Подробнее о роботах здесь и примеры. Но в этой статье речь не о них..

Исходный код теста и примеры приложений. Там 2 идентичных приложения, одно написано на ObjectiveC, второе на Titanium. Это для того, чтобы было понятно, что способ создания UI не имеет значения, если в итоге выводятся нативные iOS элементы.

Разберу тестовый пример:

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 || '');
});

Подключение модулей. wd — это стандартные биндинги для Selenium web driver. В appURL вы можете указать путь к приложению или его архиву или же его http адрес. Обработчики ивентов исключительно для вывода отладочной информации, если вам это не нужно — можете смело удалять. Теперь инициализируется вебдрайвер и выполняется тестировочная функция:

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();

И сам тест:

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;
}

В «check_invalid_position» выполняется проверка, действительно ли элемент съехал, что не вполне логично. Правильнее было бы проверять, на своем ли он месте, но assert прерывает тест при первой ошибке. Ничего страшного, главное, что я знаю, что так оно и должно быть.

Если у вас возникли трудности с заданием селектора элемента, можно воспользоваться «Appium Inspector». Для этого скачайте приложение, если используете npm версию, и нажмите на кнопку с синим информационным значком слева от кнопки «Stop». Детальнее здесь.



По ходу выполнения теста в консоль будет выводится текущее задание тестировщика.

$ 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..

Или сообщение об ошибке. Вот, что будет, если добавить проверку открытия нового модального окна по нерабочей кнопке.

  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..

Так же можно смотреть лог в самом appium



С документацией в Appium пока туговато. Но вы можете посмотреть описание и список команд в wd. Пишите комментарии или в личку, если остались вопросы. Таким образом можно переложить часть рутинной работы тестирования интерфейса мобильного приложения на плечи компьютера.
Tags:
Hubs:
+21
Comments 9
Comments Comments 9

Articles