Pull to refresh

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

Reading time 5 min
Views 9.4K
Что такое?

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

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

Человек, выполняющий тестирование, это 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()
Tags:
Hubs:
+21
Comments 15
Comments Comments 15

Articles