Динамическое (нелинейное) тестирование GUI

Что такое?

Выполнение действий над элементами графического интерфейса в случайном порядке.

Для чего нужно?

Человек, выполняющий тестирование, это Homo sapiens, т.е. он обладает неким интеллектом. Этот самый интеллект, мешает (очень редко, но мешает) ему находить «нелепости поведения» приложения связанные с непредвиденными ситуациями. Он просто не может представить себе настолько нелогичную ситуацию.
Пользователь же, намного превосходит QA в количестве и может значительно уступать ему в IQ. Отсюда, вероятность непредвиденного поведения пользователя отнюдь не крайне мала.
Итак, что нам, обладая свободными ресурсами и желанием, мешает принять меры по предотвращению подобных ситуаций? — Ничего.
Теперь сформулируем конкретные задачи, в которых «бессмысленное клацанье» по кнопкам может быть полезно:
  • Дополнить существующее тестирование стабильности приложения путем введения модели нелинейного поведения пользователя в GUI.
  • Исследовать потребление ресурсов при всех возможных вариантах работы приложения (инициированные из GUI).

Во-первых, вариант тестирования, описанный в данной статье, должен быть использован, действительно, только как дополнение к существующему тестированию графического интерфейса. Полагаться лишь на хаотичное «клацанье» по кнопкам – по меньшей мере, глупо. Нет никакой проверки, что именно происходит да и происходит ли вообще. Поэтому первый вариант можно рассматривать как дополнительное негативное тестирование на стабильность.
Второй же, максимально эффективен на бесконечном отрезке времени, что часто, невозможно. Поэтому, выбирая период измерений, следует исходить из сложности приложения, его типа и назначения. Например, наверно нет смысла 24 часа гонять «несерверное» приложение, состоящее из двух кнопок и одного чекбокса, которое умножает что-то на два, а потом результат делит пополам.

Как делать будем?

Дальнейшее описание предназначено для тестирования приложений на платформе Windows.
Предлагаю воспользоваться связкой python + pywinauto. Хотя pywinauto и имеет некоторые ограничения в плане доступа к элементам окна, для большинства случаев этого должно быть достаточно.
Честно говоря, альтернативы я не вижу. Все знакомые мне средства автоматизации тестирования GUI не обладают динамичностью, показанной ниже – уже во время выполнения теста получать список контролов, определять их тип и выполнять допустимое действие.
Также не стоит недооценивать возможностей самого Питона и его модулей. Тут вам можно и видео снять, CPU замерить и сообщение, куда надо, в случае чего отправить…

Что нам понадобится?


Еще рекомендую воспользоваться утилитой SWAPY, с помощью нее удобно смотреть свойства контролов, еще она генерирует код для pywinauto. Также с ее помощью можно проверить видит ли pywinauto контролы для вашего приложения или нет.

Спецификация теста

  1. Запускаем окно приложения.
  2. Кликаем на доступный контрол (закрываем окно).
  3. Проверяем fail criteria.
  4. Повторяем шаги 1 — 3 заданное время.
  5. По окончанию этого времени считать тест пройденным.

Fail criteria – условие, при котором тест считать проваленным. Например, запущено ли окно Crash report, не пингуется Интернет, и т.д. Тест также считать проваленным при любом эксепшене (непредвиденной ситуации).

Подводные камни

  • Иногда окно нужно закрывать.
    Пример: дочернее окно не имеет кнопки, которое его закроет и вернет фокус родительскому окну.
  • Окно может не обрабатывать Close – не фэйлить тест при этом.
    Пример: диалоговое окно только с кнопками OK и Cancel.
  • Pywinauto может не видеть элементы управления на окне.
    Тут мы ничего не сделаем – ищем другую связку. Проверяем через SWAPY или через
    pywinauto.application.WindowSpecification.PrintControlIdentifiers

Код

По спецификации:
  1. Запускаем бинарник, ожидаем появления главного окна:
    pywinauto.application.Application().start_(binary_path)
    pywinauto.timings.WaitUntil(WAIT_TIMEOUT, CHECK_INTERVAL, _check_window)

  2. С помощью enabled_and_visible() получаем список доступных контролов. Случайным образом выбираем по какому элементу кликнуть либо закрыть окно:
    if ready_contr_list and random.randint(0,len(ready_contr_list)):
      control = random.choice(ready_contr_list)
      print('Click on - "%s"' % control.Texts()[0].encode('unicode-escape', 'replace'))
      highlight_control(control)
      control.Click()
    else:
      try:
        window.Close()
      except:
        pass
      else:
        print('Close window')
    

  3. Fail criteria. Ничего в голову не пришло. Ставим заглушку:
    if 1==0:
      print('')
      result = TEST_FAILED
      break


Полный текст ниже. Хочется отметить еще несколько моментов:
  1. make_action пока умеет только посылать сигнал одинарного левого клика на контрол (или закрывать окно). Если тема будет интересна, можно будет усложнить логику.
  2. highlight_control подсвечивает активный контрол. Просто красиво.
  3. Для запуска скрипта понадобится указать:
    • Путь к исполняемому файлу. Можно с параметрами:
      BINARY_PATH = r'"C:\path\app.exe" –params 1 2 3'
    • Регулярку для заголовка главного окна:
      TITLE_RE = 'My app - .*'
    • Класс главного окна. Смотрим в SWAPY:
      CLASS_NAME = '#32770'


Результат

Тестировал родной Windows RDP клиент.
Буквально за час удалось поймать краш RDP клиента. Успех? — Наверно. Вручную повторить не удалось.
Тем не мене, дампы еще никто не отменял, так что вскрытие покажет…

Полный текст скрипта:

import pywinauto
import random
import thread
import time
import sys

'''
GUI dynamic testing
'''

TEST_FAILED = 1
TEST_PASSED = 0
TEST_EXEC_TIME = 60 * 60
WAIT_TIMEOUT = 30
CHECK_INTERVAL = 0.2
BINARY_PATH = r'"C:\WINDOWS\system32\mstsc.exe"'
TITLE_RE = 'Remote Desktop Connection'
CLASS_NAME = '#32770'

def _check_window():
  '''
  Check window is opened
  '''
  try:
    pywinauto.findwindows.find_windows(title_re=TITLE_RE, class_name=CLASS_NAME)[0]
  except:
    return False
  else:
   return True

def start_binary(binary_path):
  '''
  Start a binary, wait for window opens
  '''
  if not _check_window():
    pywinauto.application.Application().start_(binary_path)
    pywinauto.timings.WaitUntil(WAIT_TIMEOUT, CHECK_INTERVAL, _check_window)
  return 0

def get_top_window(title_re, class_name):
  '''
  Return the top window of the binary
  '''
  if not _check_window():
    start_binary(BINARY_PATH)    
  app = pywinauto.application.Application()
  try:
    app.Connect_(title_re=TITLE_RE, class_name=CLASS_NAME)
  except pywinauto.findwindows.WindowAmbiguousError:
    app.Connect_(title_re=TITLE_RE, class_name=CLASS_NAME, active_only=True)
  return app.top_window_()

def enabled_and_visible(all_conrt_list):
  '''
  Return list of ready for action controls
  '''
  ready_contr_list = []
  for contr in all_conrt_list:
    if contr.IsEnabled() and contr.IsVisible():
      ready_contr_list.append(contr)
  return ready_contr_list
  
def highlight_control(control):
  '''
  Highlight control
  '''
  def _highlight_control(control, repeat = 1):
    while repeat > 0:
      repeat -= 1
      control.DrawOutline(thickness=1)
      time.sleep(0.7)
      control.DrawOutline(colour=0xffffff, thickness=1)
      time.sleep(0.4)
  thread.start_new_thread(_highlight_control,(control,3))
  return 0
  
def make_action(window):
  '''
  Make action on a control or close a window  
  '''
  all_conrt_list = window.Children()
  ready_contr_list = enabled_and_visible(all_conrt_list)
  if ready_contr_list and random.randint(0,len(ready_contr_list)):
    control = random.choice(ready_contr_list)
    print('Click on - "%s"' % control.Texts()[0].encode('unicode-escape', 'replace'))
    highlight_control(control)
    control.Click()
  else:
    try:
      window.Close()
    except:
      pass
    else:
      print('Close window')


def main():
  '''
  main section
  '''
  start_time = time.time()
  result = -1
  
  try:
    #start testig build
    start_binary(BINARY_PATH)    
    #testing cycle
    while (time.time() - start_time) < TEST_EXEC_TIME:    
      #get top window
      window = get_top_window(TITLE_RE, CLASS_NAME)      
      #make an action
      make_action(window)      
      #check fail criteria
      if 1==0:
        print('')
        result = TEST_FAILED
        break        
    else:
      result = TEST_PASSED
      print('Test passed')        
  except Exception, e:
    result = TEST_FAILED
    print('Test failed.\n Exception %s' % e)  
  sys.exit(result)
    
if __name__ == '__main__':
    main()
Поделиться публикацией

Комментарии 15

    0
    Наверное с selenium тоже можно кнопать хаотично, надо обдумать.
      0
      Selenium Уже НЕ работает с новой версией Firefox — проверял.
      + pywinauto — это маленько вообще другая опера, если я до конца правильно понимаю, что такое Selenium.
      pywinauto — тестирование виндовозных приложений.
      Selenium — тестирование Веб'а
        0
        Ну кто про что :) Можно же покнопать и веб.
      +1
      Выход от таких тестов к сожалению очень не большой. Но подход крайне интересный.

      Немного эффективнее будет если всё же перебирать все варианты пусть и хаотично — тогда есть возможность пройти по нескольким формам какой-то осмысленный путь.

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

      Не затронутыми как я понял остаются поля для ввода, без чего многие формы вас дальше просто не пустят.
        0
        > Выход от таких тестов к сожалению очень не большой. Но подход крайне интересный.
          0
          это правда.
          0
          Пардон за коменты — я еще не совсем освоился. Исходя из личного опыта только два серьезных кейса было найдено таким способом. Но оно того стоило… А для поиска ликов — самое оно.
          На счет предложения. Это поломает концепцию случайного кликанья. Я вот подумываю о том, что бы добавить некоторые цепочки для зависимых элементов. Например, после ввода а edit нажать энтер. Но что бы оставалась случайность, следует наделить второе действие весом (вероятнее всего будет нажат энтер, но может и просто окно закрыть). Но это нужно хорошенько обдумать.
            0
            2 серьёзных кейса это лики? или что-то функциональное?
              0
              Нет, краши. Один связан с динамически появляющимися элементами. Если просто, то при попытке открыть опции элемента, которого уже нет на окне.
              Второй — это краш сервиса, который стартовал/останавливал тяжелую операцию по команде из GUI. Тут просто повезло.
              Вспомнил. Еще были зависания UI — что то с синхронизацией сервис-ui.
              А лики систематически приходили с добавлением новой функциональности.
                0
                ну тот который «один» как я понял не несёт пользы поскольку человек не сможет нажать на опцию которой уже нет на форме. т.е. опять таки пользы от этого нету.
                  0
                  Э, нет. Там был список опций с фильтром. Когда опцию выделяешь, появляется кнопка или чекбокс в зависимости от типа опции. Так вот, если выделить опцию, затем применить фильтр(что бы выбранная опция исчезла), то относящиеся к опции кнопка/чекбокс остаются на UI. Если попытаться нажать/изменить — краш.
                    0
                    Ух. Ну это тоже большая удача что нашлось таким случайным тестингом.
          0
          Так этож можно автоматизировать кучу действий в винде! Да еще все это на питоне, красота!
            0
            К сожалению не все. Навороченные контролы (html layout, и т.д.) pywinauto просто не распознает.
            И это самая большая печалька.
            0
            Карма или рейтинг позволили — переместил в Тестирование.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое