Если Вы разрабатываете более-менее сложный программный продукт, то Вам должна быть знакома ситуация, когда системные (end-to-end) тесты по тем или иным причинам автоматизировать не удаётся. На это могут быть разные причины, я приведу несколько примеров:
- У приложения нет и не может быть API, за которое можно зацепиться, по соображениям безопасности;
- Приходится поддерживать legacy-проект, про автоматизацию тестирования которого никто никогда не задумывался;
- Во время тестирования задействуется сторонний продукт, например — антивирус;
- Необходимо проверить работоспособность продукта на большом количестве различных целевых платформ;
- Тестовый стенд представляет собой сложную гетерогенную систему, включающую в себя промежуточное сетевое оборудование.
Эти и многие другие ситуации приводят к худшему кошмару любого разработчика — ручному тестированию. Самое неприятное заключается в том, что нельзя провести тестирование один раз и забыть о нём. Нет, нам приходится перед каждым релизом (а может и чаще) раскатывать виртуалки, устанавливать туда тестируемое приложение и тыкать по кнопкам снова и снова, чтобы убедиться, что мы не словили регрессию.
Если Вы ищете решение этой проблемы — то прошу под кат.
Любой тест, выполняемый на виртуальной машине, можно автоматизировать с помощью последовательности простых действий, таких как движение мышкой или нажатие клавиш на клавиатуре. Это именно те действия, которые совершает тестировщик во время ручного тестирования. Такие тесты могли бы выглядеть, грубо говоря, следующим образом:
кликнуть_мышкой_на_кнопке_с_надписью "Сохранить"
напечатать_на_клавиатуре "Hello world"
дождаться_надписи_на_экране "Готово"
При этом неважно, что именно вы тестируете: XAML-приложение, Qt-приложение, Electron-приложение, веб-страницу или вообще консольное приложение. Вы кликаете по экрану виртуальной машины и набираете текст на клавиатуре, а как приложение устроено внутри — это Вас уже совершенно не волнует. Удобно? Конечно!
Одна только загвоздка — довольно трудно понять, где на экране виртуалки находится кнопка "Сохранить", и есть ли на экране надпись "Готово". Я думаю, это одна из причин, почему мы не видим на рынке переизбытка инструментов, работающих по описанному принципу.
Однако, в последнее время технологии компьютерного зрения шагнули далеко вперёд. Искусственные нейронные сети успешно справляются с такими сложными задачами, как, например, управление автомобилем. Неужели они не справятся с такой заведомо более простой задачей, как обнаружение элементов GUI на экране виртуальной машины?
Как вы могли уже догадаться, нейросетям такая задача вполне по плечу. Вы можете сами в этом убедится, попробовав в действии новый инструмент для автоматизации системных тестов — интерпретатор тестовых сценариев Testo. Интерпретатор Testo позволяет выполнять сценарии, написанные на специализированном языке тестовых сценариев Testo Lang, которые выглядят примерно следующим образом:
mouse click "Сохранить"
type "Hello world"
wait "Готово"
Это всё, что Вам надо написать на языке Testo Lang чтобы:
- Кликнуть на надпись на экране "Сохранить";
- Напечатать на клавиатуре "Hello world";
- Дождаться появления на экране строки "Готово".
Однако, мне бы не хотелось, чтобы у Вас сложилось ложное впечатление, будто бы Testo — это какой-то аналог AutoIt или Sikuli. Нет, это не просто инструмент для автоматизации чего-то-там, это инструмент, заточенный именно под автоматизацию системных тестов. Testo берёт под свой контроль значительное количество задач, которые обычно выполняет тестировщик: определение того, какие тесты пора прогнать заново, подготовку виртуального стенда, составление отчёта о том, какие тесты свалились и в какой момент, и так далее.
Так, с этого момента поподробнее
Итак, мы в Вами говорим именно о системных (end-to-end) тестах. Системные тесты предполагают, что Вы тестируете программу не саму по себе в вакууме, а помещаете её в конкретное окружение и смотрите, как она с этим окружением справится. Под окружением может пониматься что угодно: и версия ОС, и наличие/отсутствие каких-то приложений/драйверов, и взаимодействие по сети, и соединение с Интернетом, и недостаток дискового пространства/оперативной памяти… Да и много чего ещё.
Самый удобный способ создать такое окружение для программы — это установить тестируемую программу внутрь виртуальной машины. Но виртуальную машину необходимо для начала создать и установить на неё операционную систему. Давайте посмотрим, как с этой задачей справляется платформа Testo. Мы предусмотрели специальные конструкции в языке тестовых сценариев Testo Lang, предназначенные для создания элементов виртуальной инфраструктуры Ваших стендов. Например, следующий сниппет объявляет "пустую" виртуалку:
machine my_super_vm {
ram: 2Gb
cpus: 2
iso: "ubuntu_server.iso"
disk main: {
size: 5Gb
}
}
Эта конструкция создаёт виртуальную машину с 2Гб оперативной памяти, 2 ядрами процессора и 5Гб дискового пространства. При запуске такой виртуалки, начнётся процесс установки операционной системы из образа ubuntu_server.iso
.
Это может быть несколько непривычно, но мы рассматриваем процесс установки операционной системы как ещё один тест, наравне с теми тестами, в которых проверяется собственно работоспособность Вашей программы. Это утверждение обретёт бОльший смысл, если мы на секунду представим, что мы разрабатываем не программу, а операционную систему. Может быть это какая-то специализированная система, например Alt Linux, а может быть мы разрабатываем игрушечную операционную систему just for fun. В любом случае, тестировать её как-то надо, а платформа Testo подходит для этой цели как нельзя лучше, потому что для неё нет никакой разницы, что мы тестируем: операционную систему или программу.
Так а что же делать с пустой виртуалкой? В качестве примера давайте посмотрим, как мог бы выглядеть тест, написанный на языке Testo Lang и выполняющий установку операционной системы:
test my_super_test {
my_super_vm {
start
wait "Language"
press Enter
wait "Install Ubuntu Server"
press Enter
wait "Choose the language"
press Enter
# И так далее
...
}
}
Здесь мы видим новую конструкцию языка, которая объявляет тест my_super_test
. В этом тесте учавствует всего одна виртуалка my_super_vm
. Тест начинается с включения виртуальной машины. Затем мы дожидаемся, когда на экране появится надпись "Language" и нажимаем клавишу Enter. Собственно, весь тест будет заключаться в последовательности таких действий: ждём наступления события, затем печатаем что-то на клавиатуре.
Разумеется, далеко не всегда хочется заморачиваться с установкой ОС и её первичной настройкой. Поэтому мы предусмотрели возможность импорта диска от другой виртуальной машины:
machine my_super_vm {
ram: 2Gb
cpus: 2
disk main: {
source: "prepared_vm.qcow2"
}
}
То есть вы можете вручную подготовить виртуальную машину, установить туда ОС, установить дополнительные программы, выполнить какие-то настройки, например, отключить фаервол, а затем получившийся диск от вручную созданной виртуалки использовать как начальное состояние для виртуалок из автотестов.
Подготовка виртуалки и установка ОС — это конечно всё очень хорошо, но тесты, тесты-то на мою программу где? Хорошо, давайте представим, что мы хотим протестировать инсталлятор нашей супер-программы. Представим также, что мы уже вручную подготовили виртуальную машину с Windows 10 на борту. Для простоты примера предположим, что инсталлятор нашей супер-программы уже скопирован на рабочий стол этой виртуалки. Тогда автотест на установку программы будет выглядеть следующим образом:
machine my_win_vm {
ram: 2Gb
cpus: 2
disk main: {
source: "my_windows_10.qcow2"
}
}
test my_installer_test {
my_win_vm {
# Запустим виртуальную машину
start
# Дождёмся появления рабочего стола
wait "Корзина"
mouse dclick "my_super_installator"
wait "Добро пожаловать"
mouse click "Далее"
wait "Выберите путь установки"
mouse click "Продолжить"
wait "Успешно" timeout 10m
mouse click "Закрыть"
}
}
Правда, просто? А мы только разогреваемся ...
Что за wait
такой и как он работает?
Сделаем мини перерыв и поговорим пару минут о том, как это вообще работает. Тестовые сценарии состоят по большей части из двух вещей:
- Воздействие на виртуальную машину (mouse move/click, type, press, start/stop, plug flash и много чего ещё);
- Анализ происходящего на экране (wait).
Так вот, это действие wait
и является основным видом визуальных проверок в языке Testo Lang. Действие wait
дожидается появления на экране определённых объектов и событий в течение заданного таймаута (по умолчанию — одна минута). И если событие не наступило — генерируется ошибка (прямо как человек, который ждёт надписи "Успешно" пока у него не кончится, наконец, терпение).
Если мы говорим про поиск текста на экране виртуалки (то бишь — на скриншоте), то обычно для этих целей используют какую-нибудь OCR (Optical Character Recognition) систему (например, Tesseract). Однако, это не совсем верный подход. Дело в том, что OCR-системы строятся исходя из двух постулатов:
- предполагается, что мы ничего не знаем о том, какой текст находится на изображении;
- задача системы — извлечь из изображения как можно больше информации.
В случае автотестов мы имеем совершенно иную ситуацию:
- мы знаем, какой текст должен находиться на изображении;
- задача системы намного проще — требуется сказать, присутствует ли заданный текст на изображении, и, если да — то где он находится.
В платформе Testo для реализации этой задачи мы использовали нейросети. На вход нейросетям мы подаём сразу две вещи: скриншот экрана виртуалки и искомый текст. От нейросети требуется сказать только, есть ли что-то похожее на изображении или нет. Например, если мы ищём слово "Hopa", то нам вполне подойдёт как слово, написанное кириллицей, так и слово, написанное латиницей, потому что выглядят они совершенно одинаково.
Такой подход к визуальным проверкам позволил нам добиться приемлемых результатов по точности детекта теста и, в то же время, получить хорошую скорость работы нейросетей даже на CPU. Разумеется, мы не собираемся останавливаться на достигнутом и будем и дальше совершенствовать точность нейросетей, а также добавим возможность нечёткого поиска по картинкам/иконкам.
На одном wait
далеко не уедешь
Писать длинные тесты с помощью wait
+ click
довольно муторно, особенно, если нет автоматического рекодера тестов. Тесты на основе визуальных проверок — это скорее крайний вариант, когда нет другой возможности протестировать приложение или настроить тестовое окружение. Обычно всё же существует возможность выполнить какие-то проверки путём запуска процессов на гостевой системе, например — с помощью bash-скриптов.
Как раз на этот случай мы предусмотрели специальную конструкцию в языке Testo Lang. Для её работы на виртуальной машине должны быть установлены дополнения для гостевых систем, которые постовляются вместе с интерпретатором Testo. После установки гостевых дополнений становится возможным писать такие тесты:
test my_super_test {
my_super_vm {
exec bash "echo Hello world from bash"
exec python """
print("Hello from python")
"""
}
}
Если какая-либо команда из bash-скрипта завершится с ошибкой, то и весь тест будет считаться проваленным. Аналогично, если python-скрипт вернёт не ноль, то выполнение теста сразу же завершится.
На самом деле довольно многие тестовые сценарии выглядят следующим образом. Сначала с помощью wait
+ click
на гостевую систему устанавливаются дополнения, а затем проверки сводятся к запуску процессов на виртуалке. Но при этом ничто не мешает в любой момент вернуться к визуальным проверкам. Тут всё зависит от Вас — как Вам удобнее, так и делайте.
Кроме того, гостевые дополнения позволяют легко копировать файлы между виртуалкой и хостом:
test copy_demo {
my_super_vm {
copyto "/file/on/host" "/file/on/guest"
copyfrom "/file/on/guest" "/file/on/host"
}
}
Да зачем же придумывать целый язык?
Мне кажется, многие читатели сейчас думают: "Ребят, серьёзно? Целый язык? Зачем? Ну напишите Вы библиотеку для питона или для чего ещё. Все разумные люди так делают".
На самом деле, причин для создания собственного языка у нас было много. Вот лишь несколько из них:
- Мы бы хотели, чтобы у нашего языка был минимальный порог вхождения для людей, не знакомых с программированием;
- Мы бы хотели избавиться от лишней мишуры, присущей языкам общего назначения, оставив только то, что требуется для автотестов;
- Некоторые фичи, которые мы запилили в Testo-lang, просто так не воткнешь в библиотеку для Python!
Например, Testo поддерживает кеширование тестов, благодаря чему тесты можно прогонять инкрементально — только в случае, когда это необходимо (аналогично инкрементальной компиляции программ).
Допустим, у Вас есть такой тест:
test run_installator {
my_super_vm {
copyto "/path/on/host.msi" "C:\\Users\\Testo\\Desktop\\setup.msi"
mouse dclick "setup"
...
}
}
Допустим, Вы его запустили, и он прогнался успешно. Если Вы тут же запустите его ещё раз — он отработает мгновенно! Так а действительно, зачем прогонять тест ещё раз, если:
- Сам тест не менялся;
- Сборка Вашего инсталлятора не менялась.
А вот как только Вы соберёте новую сборку своего инсталлятора и запустите тест ещё раз — то он запустится заново! Testo отслеживает все внешние файлы, участвующие в тестах, и как только в них что-то меняется, кеш соответствующих тестов сбрасывается. И да, этот механизм полностью прозрачный — Вам не надо писать ничего для того, чтобы это работало.
Ух, круто. А что ещё умеет Testo?
Одна виртуалка в тестах — это скучно. Самое веселье начинается, когда виртуалок становится много, и они начинают взаимодействовать между собой. В тестовых сценариях Вы можете создавать сколько угодно виртуалок и соединять их сетями (можно и доступ в Интернет добавить):
# Сеть для соединения двух виртуальных машин
network net1 {
mode: "internal"
}
# Сеть для доступа в Итнернет
network internet {
mode: "nat"
}
machine my_super_client {
...
nic server_side: {
attached_to: "net1"
}
nic internet: {
attached_to: "internet"
}
}
machine my_super_server {
...
nic client_side: {
attached_to: "net1"
}
}
Хотите добавить в стенд флешку? Нет проблем, пара строк и у Вас есть виртуальная флешка (можно даже скопировать на неё что-нибудь с хоста)
flash my_super_flash {
fs: ntfs
size: 2048Mb
#Папка с хоста, которую надо скопировать
folder: "/path/on/host"
}
Хотите написать реально много тестов? Нет проблем, организуйте их в иерархию! Давайте для большей конкретики рассмотрим такой набор тестов:
- Установка ОС;
- Установка гостевых дополнений;
- Копирование и установка тестируемой программы на виртуалку;
- Тестирование фичи 1;
- Тестирование фичи 2.
Очевидно, что каждый последующий тест можно запускать только после того, как успешно отработает предыдущий (кроме последних двух, их можно запускать независимо друг от друга). Поэтому мы можем выстроить такое дерево тестов:
На языке Testo Lang это будет выглядеть не сильно сложнее, чем на рисунке:
test install_os {
...
}
test install_guest_additions: install_os {
...
}
test install_app: install_guest_additions {
...
}
test test_feature_1: install_app {
...
}
test test_feature_2: install_app {
...
}
При первом запуске тестов, конечно, все тесты прогонятся от начала до конца. Допустим, при этом запуске не произошло никаких ошибок. В этом случае Testo запомнит, что все эти тесты завершились успешно и, соответственно, закешированы:
Если прямо сейчас заново запустить тесты, то они вовсе не прогонятся, так как ничего с момента последнего запуска и не поменялось. Но как только Вы соберёте новый билд своей тестирумой программы, Testo это отловит и инвалидирует кеш 3, 4 и 5 тестов:
При этом тест с установкой ОС и гостевых дополнений останутся закешированными — в них ведь ничего не поменялось, так ведь? ОС та же самая, гостевые дополнения и текст тестового сценария — тоже. Поэтому эти тесты прогоняться заново не будут. Вместо этого Testo откатит виртуалки, которые учавствуют в тестах, к тому состоянию, в котором они пребывали в конце теста install_guest_additions
.
Простой, но вполне реальный пример
Возможностей Testo может хватить на десяток статей, поэтому нам бы не хотелось сейчас Вам рассказывать про все из них. Давайте лучше притормозим и рассмотрим базовый, но реальный пример с автоматизацией тестирования простенького самописного standalone-приложения MySuperApp
.
Это приложение написано на С++ с использованием библиотеки ImGui, у него нет никаких хуков для автоматизации тестирования, но мы всё равно очень хотим на каждую сборку проверять, что оно успешно запускается на Windows 10 и высвечивает нам окошко с надписью "MySuperApp is working!".
Что ж, для начала нам понадобится виртуалка. Для этого примера мы создадим виртуалку на основе уже существующей, вручную подготовленной виртуалки, на которую мы заранее установили Windows 10:
machine my_vm {
cpus: 2
ram: 4Gb
disk main: {
source: "${QEMU_DISK_DIR}/win10.qcow2"
}
}
А как нам скопировать на виртуалку сборку с нашей программой? Давайте для этого воспользуемся флешкой!
flash my_super_flash {
fs: "ntfs"
size: 16Mb
folder: "./dist"
}
Сборку с нашей программой поместим в каталог "./dist", а Testo позаботится о том, чтобы она оказалась на флешке.
Ну а теперь пишем сам тест!
test launch_my_simple_app {
my_vm {
...
}
}
Так, стоп, а с чего начать? Да всё просто — просто записывайте все действия, которые Вы бы проделывали вручную! Для начала виртуалку надо включить.
start
Окей, а дальше? Дальше надо дождаться появления рабочего стола, конечно же:
wait "Recycle Bin" timeout 10m
Вставляем флешку
plug flash my_super_flash
Кликаем по надписи "USB Drive (E:)"
mouse click "USB Drive (E:)"
Открываем файловый менеджер:
mouse click "Open folder to view files"
Дважды кликаем по нашему приложению:
mouse dclick "MySuperApp"
Как же нам понять, что наше приложение успешно запустилось? Ну, мы знаем, что наше приложение при запуске должно высвечивать надпись "hello world". Поэтому если такая надпись появилась на экране, то это с большой долей вероятности свидетельствует о том, что всё хорошо. Это и будет наша основная проверка в тесте:
wait "hello world"
В конце теста не забываем вытащить флешку, и в сумме у нас получается такой скрипт:
test launch_my_simple_app {
my_vm {
start
wait "Recycle Bin" timeout 10m
plug flash my_super_flash
mouse click "USB Drive (E:)"
mouse click "Open folder to view files"
mouse dclick "MySuperApp"
wait "hello world"
unplug flash my_super_flash
}
}
Вот собственно и всё, наш первый тест готов. А как его запустить? Да тоже ничего сложного, главное указать путь QEMU_DISK_DIR
:
sudo testo run my_script.testo --param QEMU_DISK_DIR /var/lib/libvirt/qemu/images
Мы подготовили небольшой видеоролик, на котором выполняется запуск этого теста:
А теперь, напоследок, давайте попробуем симитировать ситуацию, когда в нашу программу закралась ошибка.
Например, по какому-то нелепому стечению обстоятельств, мы собрали MySuperApp с динамической линковкой стандартной библиотеки С++ вместо статической. Если программа собрана таким образом, то для её работы на гостевой системе должен быть установлен пакет Microsoft Visual C++ Redistributable. А мы разрабатываем standalone-приложение, которое не должно иметь никаких зависимостей. У разработчика на хостовой системе Microsoft Visual C++ Redistributable конечно же установлен, поэтому такую ошибку легко не заметить.
Итак, мы подкладываем в каталог ./dist
новую сборку нашего приложения и запускаем тесты заново. Вот что мы увидим:
При этом в выводе интерпретатора Testo будет указано, какой тест свалился и в какой именно строчке тестового сценария это произошло:
Тест свалился, ошибка выловлена!
Итоги
Системные тесты — это всегда непросто, но всегда очень ценно. Мы надеемся, что с Testo Вам будет максимально комфортно тестировать свои программы в самых разных условиях, и Вы сможете спать спокойно, зная, что теперь у заказчика Ваша программа будет себя вести так, как и задумывалось.
Скачать Testo абсолютно бесплатно без регистрации и СМС, а также ознакомиться с документацией можно у нас на сайте https://testo-lang.ru
Посмотреть больше примеров можно на youtube-канале https://www.youtube.com/channel/UC4voSBtFRjRE4V1gzMZoZuA