Автоматизированное тестирование ПО
Система автоматизированного тестирования приложений для 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 git scrot # easy_install pyuserinput
- Также потребуется приложение, в котором можно будет подготовить изображения для тестов. Можно использовать, например, GIMP.
- Загрузить самую свежую версию системы тестирования можно из git-репозитория:
$ 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 будут храниться изображения, используемые тестом.
Допустим, перед началом работы теста рабочий стол выглядит так:
Чтобы запустить Firefox, достаточно кликнуть его значок. Чтобы тест мог это сделать автоматически, сделаем снимок соотв. части экрана (или можно сделать полный снимок экрана и вырезать этот значок оттуда): . Нередко полезно взять только внутреннюю часть значка (), чтобы тест не зависел от того, на каком именно фоне значок на экране находится.
Сохраним это изображение в файле 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 текущего каталога):
python /home/user/rosa-autotest/rosatest/local.py <путь к test_firefox.py>
В результате должен запуститься Firefox:
В консоль при этом будет выведено следующее:
== Starting test test_firefox.TestFirefox.test_ff == Ran 1 test OK
Примечание. В качестве идентификатора теста используется строка имя_файла.имя_класса.имя_функции. При запуске с помощью local.py результаты теста сохраняются в подкаталоге results/имя_файла/имя_класса/имя_функции текущего каталога. Также используется подкаталог tmp/имя_файла/имя_класса/имя_функции - для хранения временных файлов при работе теста.
Пока тест всего лишь запускает Firefox, но не проверяет, успешно ли этот браузер запустился. Можно попробовать проверить, например, появилась ли на экране кнопка перехода на домашнюю страницу ().
Сделаем снимок соотв. части экрана и сохраним изображение этой кнопки в 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 новую вкладку, нажав на (это изображение нужно сохранить в 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 не откроет указанный сайт, точнее, пока где-то на экране не появится такое изображение:
Код:
self.press_key("enter") self.wait("rosa-logo")
В результате должен открыться сайт "www.rosalab.com":
Теперь можно закрыть 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".
Стоит учесть, что Firefox при закрытии может выдать предупреждение о том, что закрывается несколько вкладок (а может и не выдать):
В тесте можно, например, проверить, появилась ли на экране кнопка (изображение - 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" в конце вводимой строки.
Проверим, что Writer загрузился, т.е., что не более, чем, допустим, за минуту, на экране появилось следующее:
Можно попробовать проверять, появился ли на экране заголовок окна Writer (). Но, как показывает практика, система тестирования не всегда находит на экране такие изображения с мелким текстом на сером фоне. Для надёжности, будем искать изображение значков выделения текста жирным шрифтом, курсивом и т.п.: (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 отобразил его содержимое (, 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)
Подготовим теперь метод 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". Положение этой кнопки полезно сохранить, чтобы после выбора файла имитировать нажатие на неё. Стоит заметить также, что по умолчанию кнопка может быть выделена (, images/writer-open-selected.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)
) в соотв. местах.
Стоит учесть, что в диалоге открытия файла строку с путём к каталогу не всегда по умолчанию можно редактировать напрямую. Обычно она бывает в режиме навигации:
Если на экране есть изображение (images/writer-path-ok.png), будем считать, что строку с путём к каталогу - в режиме редактирования. Если нет - в режиме навигации. В последнем случае переключим её в режим редактирования, кликнув правой кнопкой мыши по любому из изображений (images/writer-triangle.png) и выбрав в контекстном меню пункт "Edit".
Код:
# 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)
Сотрём текущий путь к каталогу, нажав на (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.
Помимо этого, в тестах можно использовать следующие поля:
- self.srcdir - абсолютный путь к каталогу, где находится файл с кодом теста (на системе, где запускается тест);
- self.tmpdir - абсолютный путь к каталогу, где тест может хранить свои временные файлы (на тестируемой системе).