Здравствуйте! Данная статья является продолжением цикла статей, посвященных разработке для мобильной платформы Sailfish OS. В этот раз мы расскажем о том, как организовать тестирование QML-компонентов приложений, написанных для мобильных устройств. Рассмотрим все этапы от написания кода до запуска тестов на реальном устройстве.

Тестируемое приложение


В качестве примера будем рассматривать простое приложение-счётчик. Оно содержит поле, отображающее текущее значение счётчика. Если нажать на кнопку «add», то значение счётчика увеличивается на единицу. В вытягиваемом меню есть пункт для сброса значения счётчика до нуля.

Настройка приложения


Для написания тестов используется фреймворк QtTest, а конкретно QML-объект типа TestCase. С его помощью можно производить действия нажатия на экран и проверять ожидаемые значения на соответствие реальным. Следует заметить, что на момент написания статьи Sailfish SDK использует Qt версии 5.2, поэтому доступны не все методы, перечисленные в документации.

Для того, чтобы использовать фреймворк QtTest в приложении, необходимо добавить в *.yaml файл зависимость для сборки под PkgConfigBR: - Qt5Test. Далее необходимо прописать установочный путь для тестов в *.pro файле следующим образом:

tests.files = tests/*
tests.path = /usr/share/counter-application/tests
INSTALLS += tests
OTHER_FILES += tests/*

В данном примере в переменную tests.files записывается адрес директории с тестами в проекте, а в tests.path — путь, по которому эти тесты будут установлены на устройство.

Реализация тестов


Файлы тестов должны начинаться с префикса tst_. Пишутся тесты на языке QML, где корневым элементом является объект типа TestCase, внутри которого объявляются функции. Те функции, которые начинаются с префикса test_, считаются тестами и будут запущены фреймворком. Создадим для примера простой тест и поместим его в файл tst_counter.qml:

import QtQuick 2.0
import Sailfish.Silica 1.0
import QtTest 1.0

TestCase {
    function test_addition() {
        compare(2 + 2, 4);
    }
}

Для тестирования QML-компонентов приложения требуется использовать его основной элемент, который определён в файле qml/имя-проекта. Важно, что обращаться можно только к элементам, определённым в файлах с именами в формате CamelCase. Поэтому создаётся вспомогательный файл с именем подходящего формата (qml/ИмяПроекта), в который переносится всё содержимое qml/имя-проекта. А для того, чтобы приложение, как и прежде запускалось, в исходный файл вставляется элемент ИмяПроекта. В нашем случае содержимое файла counter-application.qml переносим в новый файл CounterApplication.qml. В файле counter-application.qml мы оставляем следующее:

CounterApplication { }

Теперь нам необходимо настроить TestCase для запуска тестов после загрузки приложения. Рассмотрим свойства этого объекта:

  • completed: bool — устанавливается значение true, после завершения набора тестов
  • name: string — название набора тестов для вывода отчёта
  • optional: bool — если установлен флаг true, то тест пропускается (по-умолчанию false)
  • running: bool — содержит значение true, если набор тестов выполняется
  • when: bool — необходимо установить значение true, для запуска набора тестов (по-умолчанию true)
  • windowShown: bool — содержит значение true, если компонент, содержащий TestCase был отображён

Для того, чтобы тестировать QML-компоненты нашего приложения нам необходимо поместить TestCase внутри объекта, описывающего приложение. Ранее мы выделили объект в отдельный файл и можем использовать его в других файлах. Мы должны воспользоваться свойствами when и windowShown, чтобы запускать тесты только, когда окно приложения отобразилось. Также установим имя для набора тестов в свойство name. Для наших тестов это выглядит так:

CounterApplication {
    TestCase {
        name: "Counter tests"
        when: windowShown
        function test_addition() {
            compare(2 + 2, 4);
        }
    }
}

Теперь объект CounterApplication доступен в тестах и мы можем взаимодействовать с ним и с отображаемыми им видами.

Фреймворк QtTest предоставляет методы для взаимодействия со стандартными Qt компонентами. К сожалению, компоненты из Sailfish Silica не являются стандартными, поэтому нам нужно самим писать методы для работы с ними. Для решения этой задачи мы расширяем класс TestCase, в который мы добавим методы для взаимодействия с компонентами Sailfish. Мы создаём файл SailfishTestCase.qml, в котором корневым элементом является объект TestCase. Внутри данного TestCase мы добавляем методы, которые хотим использовать внутри наших тестов. В дальнейшем в файлах с тестами мы используем вместо объекта TestCase объект SailfishTestCase и пользуемся добавленными методами.

Для начала нам необходимо найти некий элемент, отображаемый видом. В QML для доступа к элементам вида используется свойство id, но оно недоступно из вне. Поэтому для элементов, которые необходимо искать в отображаемом виде, мы устанавливаем значение свойства objectName и ищем элементы, используя его. Для поиска можно организовать рекурсивный спуск в глубину с проверкой значения свойства объекта на равенство искомому. Был реализован метод, который позволяет найти элемент, у которого некое свойство имеет заданное значение:

function findElementWithProperty(parent, propertyKey, propertyValue, exact, root) {
    if (parent.visible) {
        if (exact && parent[propertyKey] === propertyValue) return parent
        if (!exact && parent[propertyKey] !== undefined &&
                parent[propertyKey].search(propertyValue) !== -1) {
            return parent
        }
    }
    if (parent.children !== undefined && parent.visible) {
        for (var i = 0; i < parent.children.length; i++) {
            var element = findElementWithProperty(parent.children[i], propertyKey,
                                                  propertyValue, exact, false);
            if (element !== undefined) return element;
        }
    }
    if (root) {
        fail("Element with property key '" + propertyKey + "' and value '" +
             propertyValue + "' not found");
    } else {
        return undefined;
    }
}

Параметрами этого метода являются:

  1. parent — элемент, с которого необходимо начинать поиск
  2. propertyKey — свойство, значение которого проверяется
  3. propertyValue — значение свойства, которое необходимо найти
  4. exact — true, если необходимо полное соответствие искомого значения найденному, в противном случае значение ищется как подстрока
  5. root — true, если текущий элемент является стартовым

Для поиска элемента по objectName был реализован дополнительный метод, так как этот вид поиска наиболее востребован:

function findElementWithObjectName(root, name) {
    return findElementWithProperty(root, "objectName", name, true, true);
}

Ярким примером нестандартного компонента Qt служит вытягиваемое меню, которое широко используется в Sailfish приложениях. Среди методов TestCase не существует такого, который позволил бы одним вызовом выбрать элемент из такого меню, поэтому полезной оказалась следующая реализация данного поведения:

function openPullDownMenu(element) {
    var x = element.width / 2;
    var startY = element.height / 10;
    mousePress(element, x, startY);
    for (var i = 1; i <= 5; i++) {
        mouseMove(element, x, startY * i);
    }
    mouseRelease(element, x, startY * i);
}

function clickElement(element) {
    mouseClick(element, element.width / 2, element.height / 2);
    wait(1000);
}

function clickPullDownElement(parent, name) {
    openPullDownMenu(parent);
    clickElement(findElementWithObjectName(parent, name));
}

Метод openPullDownMenu(element) позволяет имитировать вытягивание меню так, как это делал бы пользователь: сначала производится нажатие на экран, а затем указатель ведётся вниз для открытия меню и отпускается. Параметром служит объект, содержащий это самое меню.

Также полезен метод clickElement(element), позволяющий нажать на указанный элемент и подождать секунду завершения действия, инициированного нажатием.

Комбинируя описанные выше методы мы создаём метод clickPullDownElement(parent, name), который и позволяет открыть меню, которое содержится в переданном методу элементе parent, и нажать на элемент, у которого значение свойства objectName равно значению параметра name.

С помощью этих методов мы можем написать тесты для нашего приложения. Первый будет увеличивать значение счётчика два раза и проверять, что значение увеличилось. Второй — увеличит значение счётчика и сбросит его, затем проверит, что оно стало равным 0.

CounterApplication {
    SailfishTestCase {
        name: "Counter tests"
        when: windowShown

        function test_counterAdd() {
            var button = findElementWithObjectName(pageStack.currentPage, "addButton");
            clickElement(button);
            clickElement(button);
            compare(findElementWithObjectName(pageStack.currentPage, "countText").text, "2");
        }

        function test_counterReset() {
            var button = findElementWithObjectName(pageStack.currentPage, "addButton");
            clickElement(button);
            clickElement(button);
            clickPullDownElement(pageStack.currentPage, "resetItem");
            compare(findElementWithObjectName(pageStack.currentPage, "countText").text, "0");
        }
    }
}

Приложение не закрывается между запусками тестов и не очищает данные. Ответственность за преднастройку и очистку данных до и после выполнения тестов целиком лежит на разработчике. В TestCase есть два метода, которые вызываются до и после выполнения каждого теста: init(), cleanup(). Данные методы должны использоваться для возвращения состояния приложения в изначальное. Также существуют методы initTestCase() и cleanupTestCase(), вызываемые один раз перед выполнением всех тестов и после соответственно.

В нашем примере необходимо обнулить значение счётчика после выполнения каждого теста, для этого добавим следующую реализацию метода cleanup():

CounterApplication {
    SailfishTestCase {
        name: "Counter tests"
        when: windowShown

        ...

        function cleanup() {
            clickPullDownElement(pageStack.currentPage, "resetItem");
        }
    }
}

После завершения каждого теста будет нажата кнопка «reset» из вытягиваемого меню.

Сборка и запуск тестов


Перед тем, как запускать тесты, необходимо собрать и развернуть приложение на устройстве (подойдёт как физическое устройство так и эмулятор). Данный процесс описан в одной из предыдущих статей цикла. Для того, чтобы иметь возможность запускать тесты на устройстве, необходимо установить два пакета с помощью команд:

pkcon install qt5-qtdeclarative-import-qttest
pkcon install qt5-qtdeclarative-devel-tools

Таким образом мы устанавливаем на устройство фреймворк QtTest, что позволит нам запускать написанные нами тесты.

Для запуска тестов мы используем утилиту qmltestrunner, которой в качестве параметра передаём путь к файлам с тестами. Выглядит это следующим образом:

/usr/lib/qt5/bin/qmltestrunner -input /usr/share/counter-application/tests/

В результате мы видим следующее:

********* Start testing of qmltestrunner *********
Config: Using QtTest library 5.2.2, Qt 5.2.2
PASS   : qmltestrunner::Counter tests::initTestCase()
PASS   : qmltestrunner::Counter tests::test_counterAdd()
PASS   : qmltestrunner::Counter tests::test_counterReset()
PASS   : qmltestrunner::Counter tests::cleanupTestCase()
Totals: 4 passed, 0 failed, 0 skipped
********* Finished testing of qmltestrunner *********

В выводе помимо добавленных нами двух тестов test_counterAdd() и test_counterReset() отображаются вызовы методов initTestCase() и cleanupTestCase().

Заключение


В результате был рассмотрен способ написания тестов для тестирования QML-компонент в приложениях для платформы Sailfish OS. В качестве примера было рассмотрено простое приложение-счётчик, исходники которого (вместе с тестами) доступны на GitHub.

Технические вопросы можно также обсудить на канале русскоязычного сообщества Sailfish OS в Telegram или группе ВКонтакте.

Автор: Сергей Аверкиев
Поделиться с друзьями
-->

Комментарии (1)


  1. kirikch
    08.02.2017 18:11
    +1

    на момент написания статьи Sailfish SDK использует Qt версии 5.2

    Сегодня появилась в раннем доступе Sailfish OS 2.1 с Qt 5.6.
    Так что, скоро и обновлённый SDK будет.