Система автоматизированного тестирования приложений для ROSA Linux позволяет проверять различные сценарии работы этих приложений, имитируя действия пользователя, такие, как:

Исходный код системы: https://abf.rosalinux.ru/spectre/rosa-autotest/

Система является надстройкой над Python unit testing framework. Также система включает в себя модифицированную версию инструмента Xpresser.

Содержание

Тестирование графических приложений

Тесты графических приложений для ROSA Linux могут работать в двух режимах:

Тесты для приложений в установленной системе, как правило, стоит писать так, чтобы один и тот же тест можно было запустить в любом их указанных режимов. Это получается автоматически, если тест использует только средства Python unittest и API, предоставляемый нашей системой.

Система тестирования использует Python 2.7.

Тестирование приложений, запущенных на отдельной машине

Примечание. Для большинства разработчиков тестов, скорее всего, будет удобнее запускать тесты не в этом режиме, а в локальном (см. ниже).

Инструкции по установке и настройке всего необходимого для этого режима тестирования находятся здесь: https://abf.rosalinux.ru/spectre/rosa-autotest/tree/master/doc

Скрипт для запуска всего набора тестов: launcher.py

Тестирование приложений, запущенных на локальной машине

Здесь предполагается, что тестирование выполняется на ROSA Desktop Fresh.

# urpmi python-distribute python-xlib opencv-devel python-opencv python-numpy python-imaging
# urpmi python-pexpect git scrot
# easy_install pyuserinput
$ git clone https://abf.rosalinux.ru/spectre/rosa-autotest.git

Допустим, система загружена в каталог /home/user/rosa-autotest.

Чтобы python нашёл нужные модули при запуске тестов, можно создать ссылки на их каталоги в /usr/lib/python2.7/site-packages/ (от имени root):

# ln -s /home/user/rosa-autotest/rosa_autotest /usr/lib/python2.7/site-packages/
# ln -s /home/user/rosa-autotest/rosatest /usr/lib/python2.7/site-packages/
# ln -s /home/user/rosa-autotest/xpresser /usr/lib/python2.7/site-packages/

Путь к модулям также можно задать с помощью переменной среды PYTHONPATH.

Проверить, что все необходимые пути указаны правильно, можно, загрузив Python и попробовав импортировать модули, например, так:

$ python -c "import rosa_autotest, rosatest, xpresser"

Если эта команда отработает без ошибок, значит, всё в порядке.

Подготовка тестов - tutorial №1

Здесь предполагается, что разработчик тестов владеет Python хотя бы на базовом уровне (см. http://docs.python.org/2.7/tutorial/index.html).

Для примера подготовим тест, который запускает Firefox, открывает новую вкладку и переходит на какой-либо сайт. Все действия в данном случае выполняются на ROSA Desktop Fresh c KDE.

Создадим файл test_firefox.py, а также - подкаталог images в том же каталоге. В images будут храниться изображения, используемые тестом.

Допустим, перед началом работы теста рабочий стол выглядит так:

Rosa autotest scr1.png

Чтобы запустить Firefox, достаточно кликнуть его значок. Чтобы тест мог это сделать автоматически, сделаем снимок соотв. части экрана (или можно сделать полный снимок экрана и вырезать этот значок оттуда): Rosa autotest ff start.png. Нередко полезно взять только внутреннюю часть значка (Rosa autotest ff start inner.png), чтобы тест не зависел от того, на каком именно фоне значок на экране находится.

Сохраним это изображение в файле ff-start.png в каталоге images

Откроем test_firefox.py в каком-либо текстовом редакторе и напишем там следующее:

import time
import rosatest

class TestFirefox(rosatest.TestCaseInstalled):
    """Tests for Firefox"""

    @rosatest.imagebased
    def test_ff(self):
        """A simple test"""
        self.load_images("images")

        self.click("ff-start")

В начале этого файла загружаются необходимые модули, затем определяется класс TestFirefox, наследник класса rosatest.TestCaseInstalled.

Классы-наследники rosatest.TestCaseInstalled используются для тестов ПО на установленной системе, наследники rosatest.TestCaseLive - на системе, загруженной в Live-режиме.

Так же, как и при работе Python unittest, в классе стоит определить одну или более функций, каждая из которых выполняет свой тест в составе данного test case. В данном случае это test_ff(). Тестовые функции, использующие поиск изображений и имитацию действий пользователя, нужно отметить декоратором @rosatest.imagebased.

self.load_images("images") загружает изображения, которые будут использоваться в тесте, из созданного нами выше подкаталога images.

self.click("ff-start") ищет на экране изображение из файла images/ff-start.png и имитирует нажатие левой кнопкой мыши в соотв. месте.

На этом этапе уже можно запустить тест и проверить, что он, действительно, запускает Firefox. Проще всего для этого использовать скрипт local.py (обратите внимание, что при этом логи теста будут сохранены в подкаталоге results текущего каталога):

LC_ALL=C python /home/user/rosa-autotest/rosatest/local.py <путь к test_firefox.py>

В результате должен запуститься Firefox:

Rosa autotest scr2.png

В консоль при этом будет выведено следующее:

== Starting test test_firefox.TestFirefox.test_ff ==

Ran 1 test
OK

Примечание. В качестве идентификатора теста используется строка имя_файла.имя_класса.имя_функции. При запуске с помощью local.py результаты теста сохраняются в подкаталоге results/имя_файла/имя_класса/имя_функции текущего каталога. Также используется подкаталог tmp/имя_файла/имя_класса/имя_функции - для хранения временных файлов при работе теста.

Пока тест всего лишь запускает Firefox, но не проверяет, успешно ли этот браузер запустился. Можно попробовать проверить, например, появилась ли на экране кнопка перехода на домашнюю страницу (Rosa autotest ff home button.png).

Сделаем снимок соотв. части экрана и сохраним изображение этой кнопки в images/ff-home-button.png. Затем добавим в функцию test_ff() следующее:

        self.wait("ff-home-button")

wait() ждёт пока на экране не появится указанное изображение. Если изображение появилось, выполнение теста продолжается, если же оно не появилось в течение заданного времени (по умолчанию, 30 секунд), wait() генерирует исключение, тест завершается и отмечается как failed.

Максимальное время ожидания (в секундах) можно задать в параметре timeout, а время между последовательными проверками, появилось ли изображение, - в параметре interval, например:

        self.wait("ff-home-button", timeout=120, interval=5)

Если задать timeout=0, wait() не будет ждать, просто проверит, есть ли изображение на экране в данный момент.

Если изображение за заданное время на экране не появилось, тест завершится с ошибкой:

== Starting test test_firefox.TestFirefox.test_ff ==

==============================
FAIL: test_ff (test_firefox.TestFirefox)
A simple test
------------------------------
Traceback (most recent call last):
  File "/usr/lib/python2.7/site-packages/rosatest/test.py", line 349, in internal
    func(self, *args, **kwargs)
  File "/home/eugene/work/tutorial/test_firefox.py", line 13, in test_ff
    self.wait("ff-home-button", timeout=1)
  File "/usr/lib/python2.7/site-packages/rosatest/test.py", line 299, in wait
    match, "Failed to find an image on the screen: " + image)
AssertionError: Failed to find an image on the screen: ff-home-button

Ran 1 test
FAILED (failures=1)

При завершении теста с ошибкой в каталог с результатами теста автоматически сохраняется снимок экрана на этот момент. Это может помочь при анализе результатов тестов. В логе теста (test.log в каталоге с результатами) также может быть полезная информация, в т.ч. сообщения, которые выводил непосредственно тест.

Примечание. Система тестирования использует стандартные assertions из Python unittest. TestCase-классы из модуля rosatest являются наследниками unittest.TestCase, так что в тестах можно использовать средства unittest и явно.

Пусть теперь тест откроет в Firefox новую вкладку, нажав на Rosa autotest ff new tab.png (это изображение нужно сохранить в images/ff-new-tab.png), и введёт там адрес "www.rosalab.com". Для этого добавим в функцию test_ff() следующее:

        self.click("ff-new-tab")

        # Wait a little for the tab to open.
        time.sleep(1)

        self.type_string("www.rosalab.com")

Остаётся нажать на "Enter" и подождать пока Firefox не откроет указанный сайт, точнее, пока где-то на экране не появится такое изображение:

Rosa autotest rosa logo.png

Код:

        self.press_key("enter")
        self.wait("rosa-logo")

В результате должен открыться сайт "www.rosalab.com":

Rosa autotest scr3.png

Теперь можно закрыть Firefox. Тест может имитировать нажатие комбинации клавиш "alt-f4":

        self.press_key("alt-f4")

Примечание. С помощью press_key() можно имитировать нажатия и отдельных клавиш ("esc", "f4" и пр.), и их комбинаций. Комбинации клавиш записываются через дефис ("ctrl-alt-v", "shift-f7", "ctrl-a" и пр.). Список обозначений клавиш приведён здесь: http://en.wikibooks.org/wiki/QEMU/Monitor#sendkey_keys. В дополнение к этому списку, для клавиши "Enter" можно использовать как обозначение "ret", так и "enter", для "Delete" - "del" и "delete", для "Insert" - "ins" и "insert".

Стоит учесть, что Firefox при закрытии может выдать предупреждение о том, что закрывается несколько вкладок (а может и не выдать):

Rosa autotest scr4.png

В тесте можно, например, проверить, появилась ли на экране кнопка Rosa autotest ff close tabs.png (изображение - images/ff-close-tabs.png), и если появилась, нажать её:

        button = self.find("ff-close-tabs", timeout=5)
        if button:
            self.click(button)

Функция find() делает почти то же самое, что и wait(). Отличие одно: если изображение не найдено на экране, find() возвращает None и исключений не выбрасывает (т.е. тест при этом не считается failed).

Если изображение на экране найдено, find() возвращает объект, который можно передать в функции click(), right_click(), double_click() и т.д. вместо имени изображения, что и продемонстрировано выше.

В итоге получился такой тест:

import time
import rosatest

class TestFirefox(rosatest.TestCaseInstalled):
    """Tests for Firefox"""

    @rosatest.imagebased
    def test_ff(self):
        """A simple test"""
        self.load_images("images")

        self.click("ff-start")
        self.wait("ff-home-button")

        self.click("ff-new-tab")

        # Wait a little for the tab to open.
        time.sleep(1)

        self.type_string("www.rosalab.com")
        self.press_key("enter")
        self.wait("rosa-logo")

        self.press_key("alt-f4")

        button = self.find("ff-close-tabs", timeout=5)
        if button:
            self.click(button)

Подготовка тестов - tutorial №2

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

В данном случае задачей теста будет запустить LibreOffice Writer, открыть в нём заранее подготовленный файл (rosa.odt) и проверить, что Writer, действительно, покажет содержимое этого файла.

Создадим файл test_files.py, а также - подкаталоги images и files в том же каталоге. Так же, как и в предыдущем tutorial, в images будут храниться изображения, используемые тестом. В files положим файл rosa.odt.

В начале test_files.py импортируем необходимые модули, определим TestCase-класс и его метод, выполняющий конкретный тест. В данном случае такой метод в классе будет один, но можно определить и несколько. Как это обычно требуется при работе с Python unittest, тестовым методам стоит давать имена, начинающиеся с "test_", - в этом случае система запуска тестов сможет найти эти методы (подробнее см. в описании модуля unittest).

В TestCase-классе могут быть и другие методы. Например, в данном tutorial всё необходимое для открытия файла в LibreOffice Writer будет вынесено во вспомогательный метод open_file().

import time
import os.path
import rosatest

class TestFiles(rosatest.TestCaseInstalled):

    @rosatest.imagebased
    def test_files(self):
        """A test to show file operations"""
        self.load_images("images")

Запустить Writer можно как кликнув по его значку в SimpleWelcome, так и, например, нажав alt-f2 и введя имя приложения в появившемся окне. Допустим, в тесте будет реализован второй вариант:

        self.press_key("alt-f2")
        time.sleep(1) # wait a little for the edit control to show up

        # Start LibreOffice Writer
        self.type_string("writer\n")

Примечание. Можно было выполнить нажатие "Enter" и отдельной командой (self.press_key("enter")), но в данном случае удобно сразу указать "\n" в конце вводимой строки.

Rosa autotest scr2 1.png

Проверим, что Writer загрузился, т.е., что не более, чем, допустим, за минуту, на экране появилось следующее:

Rosa autotest scr2 2.png

Можно попробовать проверять, появился ли на экране заголовок окна Writer (Rosa autotest writer caption.png). Но, как показывает практика, система тестирования не всегда находит на экране такие изображения с мелким текстом на сером фоне. Для надёжности, будем искать изображение значков выделения текста жирным шрифтом, курсивом и т.п.: Rosa autotest writer panel.png (images/writer-panel.png).

        self.wait("writer-panel", timeout=60, interval=5)

Примечание. Если предыдущий сеанс работы с Writer не был завершён корректно, при старте Writer может вывести окно с предложением восстановить те или иные файлы. Для простоты, в этом tutorial такие ситуации не обрабатываются.

Иногда при появлении окна Writer на экране оно не получает фокус ввода. На всякий случай кликнем на этом окне (точнее, для простоты - в центре экрана), чтобы фокус был у нужного окна:

        width, height = self.screen_size()

        # Make sure the main window has focus
        self.click(width/2, height/2)

Теперь вспомним, что тесты могут проверять ПО не только на той машине, где они сами находятся, но и на виртуальных машинах в зависимости от того, как тесты запущены. Чтобы на тестируемой системе можно было работать с файлом rosa.odt, его надо туда загрузить с помощью put_file():

        # Use put_file() to copy the file to the system under test
        path = self.put_file("files/rosa.odt", "rosa.odt")

Первый аргумент - путь к файлу на той системе, где находится тест ("files/rosa.odt"). Если это относительный путь, то он считается относительно того каталога, где лежит файл с тестом (test_files.py). Во время работы теста путь к этому каталогу хранится в self.srcdir.

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

При запуске тестов на локальной машине, который используется в этом и предыдущем tutorial'ах, put_file() просто копирует указанный файл в нужный каталог на той же системе.

put_file() возвращает абсолютный путь к файлу на тестируемой системе.

Стоит заметить, что при запуске тестов на локальной машине владельцем загруженного файла будет текущий пользователь, а при запуске для работы с приложениями в отдельной машине - root. В данном случае это не критично, но, если бы, например, потребовалось изменить этот файл в тесте, стоит добавить нужные права доступа:

        self.run_command(["chmod", "a+w", path])

Для открытия файла используем метод open_file(), подробнее о его реализации речь пойдёт ниже. Когда файл открыт, проверим, что Writer отобразил его содержимое (Rosa autotest writer text rosa.png, images/writer-text-rosa.png). Затем закроем Writer, дав ему некоторое время корректно завершить работу.

        self.open_file(path)

        self.wait("writer-text-rosa")

        # Ask Writer to close and give it some time to do it correctly
        self.press_key("alt-f4")
        time.sleep(5)

Rosa autotest scr2 6.png

Подготовим теперь метод open_file() в том же классе. Чтобы открыть файл, нажмём ctrl-o:

    def open_file(self, path):
        """Opens the specified file.

        Helper function. Assumes load_images() has been called already.
        """
        self.press_key("ctrl-o")

Проверим, что появился диалог открытия файла. Для этого можно, например, найти на экране кнопку "Open". Положение этой кнопки полезно сохранить, чтобы после выбора файла имитировать нажатие на неё. Стоит заметить также, что по умолчанию кнопка может быть выделена (Rosa autotest writer open selected.png, images/writer-open-selected.png) или не выделена (Rosa autotest writer open.png, images/writer-open.png), нужно обработать оба случая.

        # "Open" button may be selected or not, handle both cases.
        button = self.find("writer-open-selected", timeout=3)
        if not button:
            button = self.find("writer-open", timeout=3)

        self.assertIsNotNone(button)

Примечание. Как уже говорилось выше, в тестах можно использовать любые методы класса unittest.TestCase, в частности assert-методы, проверяющие то или иное условие и порождающие исключение, если это условие не выполнено. Вызов self.assertIsNotNone(button) - как раз проверка такого рода.

Теперь нужно в диалоге открытия файла выбрать файл rosa.odt. Для этого можно, например, вписать путь к каталогу (os.path.dirname(path)) и имя файла в этом каталоге (os.path.basename(path)) в соотв. местах.

Rosa autotest scr2 3.png

Стоит учесть, что в диалоге открытия файла строку с путём к каталогу не всегда по умолчанию можно редактировать напрямую. Обычно она бывает в режиме навигации:

Rosa autotest scr2 4.png

Если на экране есть изображение Rosa autotest writer path ok.png (images/writer-path-ok.png), будем считать, что строка с путём к каталогу - в режиме редактирования. Если нет - в режиме навигации. В последнем случае переключим её в режим редактирования, кликнув правой кнопкой мыши по любому из изображений Rosa autotest writer triangle.png (images/writer-triangle.png) и выбрав в контекстном меню пункт "Edit".

Rosa autotest scr2 5.png

Код:

        # Now, the area for the directory path may be in navigation or in
        # editing mode. If the latter is the case, switch to the editing
        # mode.
        if not self.find("writer-path-ok", timeout=0):
            # Assuming we are in navigation mode, switch to editing.
            self.right_click("writer-triangle")
            time.sleep(1)

            # To the 3rd item in the context menu
            self.press_key("down")
            self.press_key("down")
            self.press_key("down")
            self.press_key("enter")
            time.sleep(0.5)

Сотрём текущий путь к каталогу, нажав на Rosa autotest writer path erase.png (images/writer-path-erase.png), впишем путь к нужному каталогу:

        self.click("writer-path-erase")
        time.sleep(0.5)

        self.type_string(os.path.dirname(path))
        self.press_key("enter")
        time.sleep(0.5)

Чтобы найти область ввода, куда нужно вписать имя открываемого файла, воспользуемся тем, что справа от неё находится как раз кнопка "Open", положение которой мы сохранили выше в переменной button. Кликнем левой кнопкой мыши, допустим, на 100 пикселов левее центра этой кнопки и попадём, куда нужно:

        # Click the edit box for the file name, to the left of "Open".
        self.click(button, dx=-100)
        time.sleep(0.5)

Методам click(), right_click(), double_click() и т.п. можно передать, помимо изображения, необязательные параметры dx и dy и задать таким образом смещение (в пикселах) от центра этого изображения.

Примечание. Ось X идёт слева направо, ось Y - сверху вниз, (0, 0) - левый верхний угол экрана.

Удалим старое имя файла, если оно было в поле ввода, введём имя нужного файла и нажмём кнопку "Open":

        # Erase the old file name (if any)
        self.press_key("ctrl-a")
        self.press_key("del")
        self.type_string(os.path.basename(path))

        self.click(button)

Полный код теста, подготовленный в этом tutorial, выглядит следующим образом:

import time
import os.path
import rosatest

class TestFiles(rosatest.TestCaseInstalled):

    @rosatest.imagebased
    def test_files(self):
        """A test to show file operations"""
        self.load_images("images")

        self.press_key("alt-f2")
        time.sleep(1) # wait a little for the edit control to show up

        # Start LibreOffice Writer
        self.type_string("writer\n")
        self.wait("writer-panel", timeout=60, interval=5)

        width, height = self.screen_size()

        # Make sure the main window has focus
        self.click(width/2, height/2)

        # Use put_file() to copy the file to the system under test
        path = self.put_file("files/rosa.odt", "rosa.odt")
        self.open_file(path)

        self.wait("writer-text-rosa")

        # Ask Writer to close and give it some time to do it correctly
        self.press_key("alt-f4")
        time.sleep(5)

    def open_file(self, path):
        """Opens the specified file in LibreOffice Writer.

        Helper function. Assumes load_images() has been called already.
        """
        self.press_key("ctrl-o")

        # "Open" button may be selected or not, handle both cases.
        button = self.find("writer-open-selected", timeout=3)
        if not button:
            button = self.find("writer-open", timeout=3)

        self.assertIsNotNone(button)

        # Now, the area for the directory path may be in navigation or in
        # editing mode. If the latter is the case, switch to the editing
        # mode.
        if not self.find("writer-path-ok", timeout=0):
            # Assuming we are in navigation mode, switch to editing.
            self.right_click("writer-triangle")
            time.sleep(1)

            # To the 3rd item in the context menu
            self.press_key("down")
            self.press_key("down")
            self.press_key("down")
            self.press_key("enter")
            time.sleep(0.5)

        self.click("writer-path-erase")
        time.sleep(0.5)

        self.type_string(os.path.dirname(path))
        self.press_key("enter")
        time.sleep(0.5)

        # Click the edit box for the file name, to the left of "Open".
        self.click(button, dx=-100)
        time.sleep(0.5)

        # Erase the old file name (if any)
        self.press_key("ctrl-a")
        self.press_key("del")
        self.type_string(os.path.basename(path))

        self.click(button)

API для тестов

Более подробное описание API для тестов можно найти в файле test.py в комментариях к соотв. методам класса TestCase.

Также можно использовать методы класса unittest.TestCase.

Помимо этого, в тестах можно использовать следующие поля:

Подготовка отчётов в формате HTML

В системе тестирования есть средства для подготовки HTML-отчётов по результатам тестов. Соотв. API реализован в файле rosa_autotest/report.py.

Для работы генератору отчётов необходимы модули PyYAML и Jinja2. Чтобы установить их, достаточно выполнить следующие команды:

# urpmi python-yaml
# easy_install Jinja2

Как отмечалось выше, результаты работы тестов сохраняются в подкаталоге results/имя_группы_тестов/имя_класса/имя_функции/ текущего каталога (<имя_группы_тестов> - имя файла с исходным кодом тестов, без '.py').

Для подготовки HTML-отчёта по результатам группы тестов, достаточно выполнить команду:

$ python /home/user/rosa-autotest/rosa_autotest/report.py results/ <имя_группы_тестов>

В результате в каталоге results/ будет создан файл results_<имя_группы_тестов>.html.

Полезные советы

Запуск команд в определённом каталоге

run_command() пока не позволяет указать явно каталог, в котором нужно выполнить команду. Можно в тесте использовать такой обходной путь:

    self.run_command("sh -c 'cd <каталог> && <команда>'")