Hello! This article is a continuation of a series of articles devoted to the development of the mobile platform Sailfish OS. This time we will talk about how to organize testing QML-components of applications written for mobile devices. Consider all the steps from writing code to running tests on a real device.
Test application
As an example, we will consider a simple application counter. It contains a field displaying the current value of the counter. If you click on the "add" button, the counter value is increased by one. In the pull-out menu there is an item to reset the counter value to zero.
Application setup
The
QtTest framework, specifically the QML object of type
TestCase, is used to write tests. With it, you can make actions of pressing the screen and check the expected values against the real. It should be noted that at the time of writing the article, the Sailfish SDK uses Qt version 5.2, so not all methods listed in the documentation are available.
In order to use the QtTest framework in an application, you need to add a dependency in the * .yaml file for building under PkgConfigBR:
- Qt5Test
. Next, you need to register the installation path for the tests in the * .pro file as follows:
tests.files = tests/* tests.path = /usr/share/counter-application/tests INSTALLS += tests OTHER_FILES += tests/*
In this example, the variable
tests.files contains the address of the directory with tests in the project, and
tests.path the way in which these tests will be installed on the device.
')
Implementation of tests
Test files must begin with the prefix
tst_ . QML tests are written, where the root element is an object of type
TestCase , inside which functions are declared. Those functions that start with the
test_ prefix are considered tests and will be run by the framework. For example, create a simple test and place it in the
tst_counter.qml file:
import QtQuick 2.0 import Sailfish.Silica 1.0 import QtTest 1.0 TestCase { function test_addition() { compare(2 + 2, 4); } }
To test QML components of an application, you need to use its main element, which is defined in the
qml / project-name file. It is important that you can apply only to elements defined in files with names in the CamelCase format. Therefore, an auxiliary file is created with the name of a suitable format (
qml / ProjectName ) in which the entire contents of the
qml / project_name are transferred. And in order to start the application, the
ProjectName element is inserted into the source file as before. In our case, the contents of the file
counter-application.qml are transferred to the new
CounterApplication.qml file. In the file
counter-application.qml, we leave the following:
CounterApplication { }
Now we need to configure TestCase to run the tests after the application is loaded. Consider the properties of this object:
- completed : bool - set to true after completion of the test suite
- name : string - the name of the test suite for report output
- optional : bool - if the flag is true , then the test is skipped (by default, false )
- running : bool - true if the test suite is running
- when : bool - must be set to true to run the test suite ( true by default)
- windowShown : bool - true if the component containing the TestCase was displayed
In order to test the QML components of our application, we need to put
TestCase inside the object that describes the application. Earlier we selected the object in a separate file and can use it in other files. We must use the
when and
windowShown properties to run tests only when the application window is displayed. Also set the name for the test suite in the
name property. For our tests, it looks like this:
CounterApplication { TestCase { name: "Counter tests" when: windowShown function test_addition() { compare(2 + 2, 4); } } }
Now the CounterApplication object is available in tests and we can interact with it and with the views it displays.
The QtTest framework provides methods for interacting with standard Qt components. Unfortunately, components from Sailfish Silica are not standard, so we need to write our own methods for working with them. To solve this problem, we extend the
TestCase class, to which we add methods for interacting with Sailfish components. We create the
SailfishTestCase.qml file in which the root element is the
TestCase object. Inside this
TestCase, we add the methods we want to use inside our tests. Later in the test files, we use the
SailfishTestCase object instead of the
TestCase object and use the added methods.
First we need to find some element displayed by the view. In QML, the
id property is used to access view elements, but it is not accessible from the outside. Therefore, for elements that need to be searched in the displayed form, we set the value of the
objectName property and look for elements using it. For a search, you can organize a recursive descent into depth with checking the value of the property of the object for equality to the desired one. A method has been implemented that allows you to find an element for which a certain property has a specified value:
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; } }
Parameters of this method are:
- parent - the element to start the search with.
- propertyKey - the property whose value is being checked
- propertyValue - the value of the property to be found
- exact - true if you want full compliance of the desired value to the one found, otherwise the value is searched as a substring
- root - true if the current item is the start item
To search for an item by
objectName , an additional method was implemented, since this type of search is most in demand:
function findElementWithObjectName(root, name) { return findElementWithProperty(root, "objectName", name, true, true); }
A prime example of a non-standard Qt component is the pull-out menu, which is widely used in Sailfish applications. Among the
TestCase methods, there is no one that would allow a single call to select an element from such a menu, so the following implementation of this behavior was helpful:
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)); }
The
openPullDownMenu (element) method allows you to imitate the stretching of the menu as the user would: the screen is pressed first, and then the pointer is held down to open the menu and released. The parameter is the object containing this menu itself.
The
clickElement (element) method is also useful, allowing you to click on the specified element and wait a second to complete the action initiated by pressing.
Combining the methods described above, we create the
clickPullDownElement (parent, name) method, which allows you to open the menu that is contained in the
parent element passed to the method and click on the element whose
objectName property
value is equal to the
name parameter value.
Using these methods, we can write tests for our application. The first will increase the counter value twice and check that the value has increased. The second will increase the value of the counter and reset it, then check that it has become equal to 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"); } } }
The application does not close between running the tests and does not clear the data. Responsibility for pre-tuning and data cleansing before and after performing tests rests entirely with the developer. There are two methods in
TestCase that are called before and after the execution of each test:
init () ,
cleanup () . These methods should be used to return the state of the application to its original state. There are also
initTestCase () and
cleanupTestCase () methods that are called once before and after all the tests, respectively.
In our example, you need to reset the counter after each test, for this we add the following implementation of the
cleanup () method:
CounterApplication { SailfishTestCase { name: "Counter tests" when: windowShown ... function cleanup() { clickPullDownElement(pageStack.currentPage, "resetItem"); } } }
Upon completion of each test, the “reset” button from the pullout menu will be pressed.
Build and run tests
Before you run the tests, you need to build and deploy the application on the device (both a physical device and an emulator will do). This process is described
in one of the previous articles of the cycle. In order to be able to run tests on the device, you need to install two packages using the commands:
pkcon install qt5-qtdeclarative-import-qttest pkcon install qt5-qtdeclarative-devel-tools
Thus, we install the QtTest framework on the device, which will allow us to run the tests we have written.
To run the tests, we use the qmltestrunner utility, which as a parameter is passed the path to the files with tests. It looks like this:
/usr/lib/qt5/bin/qmltestrunner -input /usr/share/counter-application/tests/
As a result, we see the following:
********* 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 *********
In the output, in addition to the two tests we added,
test_counterAdd () and
test_counterReset () , calls to the
initTestCase () and
cleanupTestCase () methods are
displayed .
Conclusion
As a result, a method for writing tests for testing QML components in applications for the Sailfish OS platform was considered. As an example, a simple counter-application was considered, the source code of which (along with tests) are available on
GitHub .
Technical issues can also be discussed on
the Sailfish OS Russian-speaking community channel in a Telegram or
VKontakte group .
Author: Sergey Averkiev