📜 ⬆️ ⬇️

Development for Sailfish OS: QML Components Testing

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:


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:

  1. parent - the element to start the search with.
  2. propertyKey - the property whose value is being checked
  3. propertyValue - the value of the property to be found
  4. exact - true if you want full compliance of the desired value to the one found, otherwise the value is searched as a substring
  5. 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

Source: https://habr.com/ru/post/320808/


All Articles