Разработка для 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;
}
}
Параметрами этого метода являются:
- parent — элемент, с которого необходимо начинать поиск
- propertyKey — свойство, значение которого проверяется
- propertyValue — значение свойства, которое необходимо найти
- exact — true, если необходимо полное соответствие искомого значения найденному, в противном случае значение ищется как подстрока
- 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 или группе ВКонтакте.
Автор: Сергей Аверкиев