пятница, 11 ноября 2011 г.

UIAutomation. Ограничение одного теста{!|?}

Сегодня хочется рассказать про еще один трюк, облегчающий жизнь.

Вообще, при знакомстве с UIAutomation возникает большое количество вопросов. Одним из них, и, как по мне, наиболее важным, является то, что нативно инструмент поддерживает только один тест.

Выглядит это так. Вы просто указываете файл с именем теста в окне инструмента. И выполняться будет ровно этот один тест. Если хотите другой тест, - останавливаете текущий, указываете новый и запускаете его на выполнение. И так до тех пор пока у вас не кончатся тесты...

Мое терпение лопнуло раньше. Посему, поделюсь как удалось решить эту проблему.

Для начала, расскажу о директиве #import, которую Apple добавила в свою реализацию JS для этого инструмента. Директива имеет сложное и непонятное название, а действие ее сводится к подключению других JS файлов с какими-нибудь нужными классами, фукнциями и даже данными. Если файл лежит в той же папке, что и исходный, просто укажите его имя в двойных кавычках. Если нет, - указываете относительный путь к нему. Например, так:

#import "Test.js";

или так

#import "Tests/SmokeTest.js";

Эта команда позволит нам в будущем строить нормальный фреймворк для нашего приложения.

Теперича хочется чуток подотвлечься и поговорить о том, что такое тест, но только не тест вообще, а автомейшен тест. По большому счету, это просто набор команд с входящими и исходящими данными. Входящие данные - это, обычно, какие-нибудь инициализационные значения, исходящие - результат теста в виде pass/fail (если кому нравится можно хоть в булевых значениях определять :) Еще у теста должно быть имя, дабы отличить эту штуковину от собратьев. Если кому это напомнило функцию, то это, как говорил Винни, неспроста - функция и есть, ну, или метод.

Так, ладненько, теперь так же быстро перепрыгнем на организацию тестовых наборов. Мне лично (не удивительно, почти 100% стандарт) очень по душе концепция тест-классов и тест-методов, согласно которой у вас есть несколько тест-классов, содержаших, в свою очередь, набор методов - тестов. Каждый класс относится или к какой-либо однородной группе функциональности, или объединяет тесты, схожие по реализации. Лучше, когда выбран первый вариант, который про однородность функционала, - жить потом проще :)

Ффух, наотступались. Кстати, за что люблю такие отступления, так это за то, что потом можно, например, не объяснять, что за классы и методы я сейчас буду писать :) Для облегчения работы с тестами напишем простенький класс, в котором будем хранить имя теста и результат его прогона:

/** @class Represents a test
* @author Sergey Lyopka
*/
function Test(/**String*/Name, /**bool*/Result) {
    var name = Name;
    var result = Result;

    /** Returns the test name.
    * @type String
    */
    this.getName = function () { return name; }

    /** Returns the test result.
    * @type bool
    */
    this.getResult = function () { return result; }
}
Я не буду особо расписывать, идея, думаю, понятна, а реализовать можно как угодно. Замечу только, что это не тест-метод, просто некоторая сущность, используемая для удобства.

Теперь, давайте посмотрим собственно на тест-класс и тест метод в нем:

// Test Class for FileManager tests
function FileManagerTests() {
    /*Covered test cases:
    * 16756
    */
    this.test16756 = function (log) {
        log.message("Covered Test Cases: 16756")
        testResult = true;

        var window = new MainWindow();
        waitForVisible(window.NavBar(), 10);

        if (!window.Table()) { log.debug("Table view was not found."); testResult = false; }
        else {
            if (1 != window.Table().cells().length) { 
              log.debug("Unexpected number of visible table cells was found: " +
                window.Table().cells().length); testResult = false
            }
            if (!window.LocalFolder()) { 
              log.debug("Local Folder was not found"); 
              testResult = false
            }
        }
        return testResult;
    } //test16756
}

В принципе, тоже все понятно: тест-класс FileManagerTests, сождержащий тест-метод test16756. Если код метода вызывает некоторые сомнения по поводу того, поддерживаются или нет такие функции как, скажем, waitForVisible, просто не обращайте внимания, - это реальный тест, который успользует кучу утилитных фукнций-оберток, которые были дописаны для облегчения работы с тестами. Даст Бог, и про них как-нибудь расскажу.

Осталось показать, как это все выполняется. Вспоминаем, что UIA может выполнять только один тест за раз. Вот в коде файла, который был предназначен для этого самого единственного теста, мы можем теперь вызывать наши классы и тесты, собственно вот так:

#import "Lib/TestSet.js";
#import "Lib/Logger.js";
#import "Tests/SmokeTest.js";

//Logger Init
var log = new Logger(true, false);
log.start("Smoke Test");

//Test Set filling
var testSet = new TestSet();
testSet.addTests(new SmokeTest(), log); //собственно, это и есть добавление

//Test Results
tests = testSet.getTestSet();

for(test in tests)
  if (tests[test].getResult()) log.pass(tests[test].getName());
  else log.fail(tests[test].getName());
 
if(testSet.getResult()) log.message("Test Set has completed successfully.");
else log.message("Test Set has completed unsuccessfully.");

Чтобы до конца понять, как строка testSet.addTests(new SmokeTest(), log); способствует выполнению тестов, осталось рассмотреть вспомогательный класс TestSet, который служит для формирования тестовых наборов.

#import "Test.js";


/** @class Represents a test set.
  * @author Sergey Lyopka
  */
function TestSet(){
  var tstSet = new Array();
  var result = true;
 
  /** Adds single test to the test set.
    * @param {String} testName The name of the adding test.
    * @param {bool} testResult The result the test execution.
    * @type null
    */
  this.addTest = function(testName, testResult){
    if(!testResult) result = testResult;
    tst = new Test(testName, testResult);
    tstSet.push(tst);
  }// addTest

  /** Adds entire test class to the test set.
    * @param {Test} testClass The name of the adding test class.
    * @param {Logger} log The current thread logger.
    * @type null
    */
  this.addTests = function(testClass, log){
    var testNames = getTests(testClass);
    for(var i in testNames){
      log.message(testNames[i] + " started.");
      try{
        this.addTest(testNames[i], testClass[testNames[i]](log));
      }
      catch(err){
        log.issue(testNames[i] + " test has terminated abnormally!");
        this.addTest(testNames[i], false);
      }//catch
      log.message(testNames[i] + " finished.");
    }
  }// addTests
 
  /** Returns the result of test set execution.
    * If all the tests passed, then result is true.
    * If at least one test failed, then result is false.
    * @type bool
    */
  this.getResult = function(){return result;}
 
  /** Returns array with all the tests.
    * @type Array
    */
  this.getTestSet = function(){return tstSet;}
   
  /** Gets all the tests (methods with 'test' at the name) from the test class. Internal.
    * @param {Test} testClass
    * @type Array
    * @ignore
    */
  function getTests(testClass){
    var testNames = new Array();
    for (var i in testClass) {
      name = i.toString();
      if(name.match("test")) testNames.push(name);
    } //for
    return testNames;
  } //getTests
}

Вот тут слегка поясню. От этого тест-сета мне нужны несколько свойств:
  1. Возможность добавлять тесты по одному. Очень полезная возможность, скажем на этапе дебага, или выборочного формирования набора для прогона. Для этого был сделан метод this.addTest.
  2. this.addTests метод уже служит для добавления всех тест методов какого-либо тест-класса. Для того, чтобы получить эти методы используется функция getTests. она пробегается по всем элементам класса (спасибо создателям JS за такую возможность) и выбирает методы, в имени которых есть "test". Это ограничение было введено, чтобы иметь возможность добавлять в классы какие-либо вспомогательные функции и быть уверенным, что они не будут распознаны как тесты.
  3. Возможность получить результаты прогона тест-набора.
Остальное, думаю тоже особо пояснять не надо. Разве что вот это this.addTest(testNames[i], testClass[testNames[i]](log));. Тут может быть интересна конструкция testClass[testNames[i]](log): во первых, эта строка собственно и вызывает i-й тест-метод, во-вторых по ее исполнения мы получаем результат выполнения теста.
Про параметр log можно пока не здумываться, я про это расскажу отдельно, он нужен, чтобы использовать кастомизированное логирование, но опять же, на данный момент это не важно, можете его хоть убрать, если сильно глаза режет :)


Собственно, это все, что я хотел рассказать на сегодня. Я уверен, что это решение можно сильно улучшить, но мне его хватает для того, чтобы нормально структурировать свои тест наборы и относительно прозрачно ими управлять.

Если улучшите, поделитесь, - будет повод написать о рефакторинге тестов :)

четверг, 10 ноября 2011 г.

Про маленькую Землю и самообман...

Недавно ко мне постучалась подруга и рассказала, что ee коллега нарыл блог про автоматизированное тестирование под iOS, "да вот только автор начал писать и забил, обидно так :) ".

Это вот про мой блог было... И если коллеге подруги было обидно, то мне стало стыдно, несмотря на то, что с формулировкой "забил" я бы не согласился, скорее отложил на "пока не появится больше времени" (я, однако, силен в самообмане:)

Скоро исправлюсь, обещаю!