Меню

Ошибка изменения состояния элемента

I’m using Autoruns v11.70, and when I try to deselect an item in the Logon tab, I receive the error:

Error changing item state: The system cannot find the file specified.

enter image description here

The item remains ticked, and I cannot deselected it.

Autoruns is not installed, it runs as a standalone utility.

Any ideas on how to resolve this?

asked Nov 28, 2013 at 10:02

Steve's user avatar

Given your previous question, it sounds as though Skype and Teamviewer have changed the mechanism by which they start. Look for them under another tab.

Autoruns retains a link to the blocked file within the registry. This is in a subfolder that it creates below the normal startup folder. If the file that is being blocked has been deleted, because the startup mechanism has changed, then Autoruns won’t be able to remove its entry.

I would delete the Autoruns subfolder manually using Regedit then start again. You’re looking for both:

HKCUSoftwareMicrosoftWindowsCurrentVersionRunAutorunsDisabled
HKLMSoftwareMicrosoftWindowsCurrentVersionRunAutorunsDisabled

answered Nov 28, 2013 at 10:26

Chenmunka's user avatar

ChenmunkaChenmunka

3,22813 gold badges28 silver badges37 bronze badges

1

I’m using Autoruns v11.70, and when I try to deselect an item in the Logon tab, I receive the error:

Error changing item state: The system cannot find the file specified.

enter image description here

The item remains ticked, and I cannot deselected it.

Autoruns is not installed, it runs as a standalone utility.

Any ideas on how to resolve this?

asked Nov 28, 2013 at 10:02

Steve's user avatar

Given your previous question, it sounds as though Skype and Teamviewer have changed the mechanism by which they start. Look for them under another tab.

Autoruns retains a link to the blocked file within the registry. This is in a subfolder that it creates below the normal startup folder. If the file that is being blocked has been deleted, because the startup mechanism has changed, then Autoruns won’t be able to remove its entry.

I would delete the Autoruns subfolder manually using Regedit then start again. You’re looking for both:

HKCUSoftwareMicrosoftWindowsCurrentVersionRunAutorunsDisabled
HKLMSoftwareMicrosoftWindowsCurrentVersionRunAutorunsDisabled

answered Nov 28, 2013 at 10:26

Chenmunka's user avatar

ChenmunkaChenmunka

3,22813 gold badges28 silver badges37 bronze badges

1

Меня зовут Дмитрий Макаренко, я Mobile QA Engineer в Badoo и Bumble: занимаюсь тестированием новой функциональности в наших приложениях вручную и покрытием её автотестами. 

За последние два года подход к автоматизации тестирования в нашей компании сильно изменился. Количество людей, активно вовлечённых в разработку тестов, увеличилось с десяти до 40 человек. А любая новая функциональность в приложениях теперь обязательно должна быть покрыта тестами до релиза. 

В таких условиях нам очень важно разрабатывать тесты настолько быстро, насколько это возможно. И делать их при этом стабильными — чтобы поддержка не отнимала у нас много времени. Мы решили поделиться практиками, которые помогают нам ускорять разработку тестов и повышать их стабильность. 

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

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

Напомню, что примеры из обеих частей в равной степени актуальны для тех, кто только начинает внедрять автоматизацию тестирования в своём проекте, и тех, кто уже активно ею занимается.

Спойлер

В конце статьи будет ссылка на тестовый проект со всеми практиками.

Практика 4. Верификация изменения состояния элементов

Пожалуй, эта практика одна из самых важных в мобильной автоматизации в принципе, потому что в приложениях очень редко встречаются статические элементы, которые сразу же отображаются на экране и всегда на нём доступны. Чаще мы сталкиваемся с тем, что загрузка элементов занимает какое-то время. 

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

Таким образом, прежде чем начать проверять элементы, нам необходимо дождаться их появления на экране. Естественно, эта проблема не нова и существуют стандартные решения. Например, в Selenium это различные типы методов wait, а в Calabash — метод wait_for. 

Зачем нам свой велосипед, или Почему мы отказались от стандартного решения

Когда мы начинали строить наш фреймворк автоматизации, мы использовали стандартный метод wait_for для ожидания появления или изменения состояния элементов. Но в какой-то момент столкнулись с проблемой. Иногда, раз в две-три недели, у нас зависали все тесты. Мы не могли понять, как и почему это происходит и что мы делаем не так.

После того как мы добавили логи и проанализировали их, оказалось, что эти зависания были связаны с реализацией метода wait_for, входящего в состав фреймворка Calabash. wait_for использует метод timeout модуля Ruby Timeout, который реализован на глобальном потоке. А тесты зависали, когда этот метод timeout использовался вложено в других методах: наших и фреймворка Calabash. 

Например, рассмотрим прокрутку страницы профиля до кнопки блокировки пользователя. 

def scroll_to_block_button
  wait_for(timeout: 30) do
    ui.scroll_down
    ui.wait_until_no_animation
    ui.element_displayed?(BLOCK_BUTTON)
  end
end

Мы видим, что используется метод wait_for. Происходит прокрутка экрана вниз, потом ожидание окончания анимации и проверка отображения кнопки блокировки. 

Рассмотрим реализацию метода wait_until_no_animation.

def wait_until_no_animation
  wait_for(timeout: 10) do
    !ui.any_element_animating?
  end
end

Метод wait_until_no_animation реализован так же с wait_for. Он ждёт, когда на экране закончится анимация. Получается, что wait_for, вызванный внутри wait_for, вызывает другие методы. Представьте себе, что вызовы wait_for также есть внутри методов Calabash. С увеличением цепочки wait_for внутри wait_for внутри wait_for риск зависания увеличивается. Поэтому мы решили отказаться от использования этого метода и придумать своё решение.

Требования к нему совпадали с требованиями к стандартным решениям. Нам нужен был метод, который бы повторял проверку до тех пор, пока не выполнится заданное условие либо пока не истечёт отведённое время. Если проверка не проходит успешно за отведённое время, наш метод должен выбрасывать ошибку.

Сначала мы создали модуль Poll с одним методом for, который повторял стандартный метод wait_for. Со временем собственная реализация позволила нам расширять функциональность модуля по мере того, как у нас появлялась такая необходимость. Мы добавили методы, ожидающие конкретные значения заданных условий. Например, Poll.for_true и Poll.for_false явно ожидают, что исполняемый код вернёт true либо false. В примерах ниже я покажу использование разных методов из модуля Poll.

Также мы добавили разные параметры методов. Рассмотрим подробнее параметр return_on_timeout. Его суть в том, что при использовании этого параметра наш метод Poll.for перестаёт выбрасывать ошибку, даже если заданное условие не выполняется, а просто возвращает результат выполнения проверки. 

Предвижу вопросы «Как это работает?» и «Зачем это нужно?». Начнём с первого. Если в методе Poll.for мы будем ждать, пока 2 станет больше, чем 3, то мы всегда будем получать ошибку по тайм-ауту. 

Poll.for { 2 > 3 }
> WaitError

Но если мы добавим наш параметр return_on_timeout и всё так же будем ждать, пока 2 станет больше, чем 3, то после окончания тайм-аута, 2 всё ещё не станет больше, чем 3, но наш тест не упадёт, а метод Poll.for вернёт результат этой проверки. 

Poll.for(return_on_timeout: true) { 2 > 3 }
> false

Зачем это нужно? Мы используем параметр return_on_timeout для верификации изменения состояния наших элементов. Но очень важно делать это аккуратно, так как он может скрывать реальные падения тестов. При некорректном использовании тесты будут продолжать выполняться, когда заданные условия не выполняются, в тех местах, где должны были бы выбросить ошибку. 

Варианты изменения состояния элементов

А теперь перейдём к самому интересному — поговорим о том, как проверять различные изменения состояния и какие изменения состояния вообще существуют. Познакомьтесь с нашим объектом тестирования — чёрным квадратом: 

Он умеет всего две вещи: появляться на экране и пропадать с экрана. 

Первый вариант изменения состояния называется «Должен появиться». Он происходит в том случае, когда состояние 1 – на экране нет нашего объекта тестирования, а состояние 2 – он должен появиться. 

Должен появиться

Должен появиться

Если он появляется, то проверка проходит успешно. 

Второй вариант изменения состояния называется «Должен пропасть». Происходит он тогда, когда в состоянии 1 отображается наш объект тестирования, а в состоянии 2 его быть не должно. 

 Должен пропасть

Должен пропасть

Третий вариант не такой очевидный, как первые два, потому что в нём, по сути, мы проверяем неизменность состояния. Называется он «Не должен появиться». Это происходит, когда в состоянии 1 наш объект тестирования не отображается на экране и спустя какое-то время в состоянии 2 он всё ещё не должен появиться. 

 Не должен появиться

Не должен появиться

Вы, наверное, догадались, какой вариант — четвёртый. Он называется «Не должен пропасть». Происходит это, когда в состоянии 1 объект отображается на экране, и спустя какое-то время в состоянии 2 он всё ещё находится там.

Не должен пропасть

Не должен пропасть

Реализация проверок разных вариантов

Мы зафиксировали все возможные варианты изменения состояния элементов. Как же их проверить? Разобьём реализацию на проверки первых двух вариантов и проверки третьего и четвёртого. 

В случае с первыми двумя вариантами всё довольно просто. Для проверки первого нам просто нужно подождать, пока элемент появится, используя наш метод Poll: 

# вариант "Должен появиться"
Poll.for_true { ui.elements_displayed?(locator) }

Для проверки второго — подождать, пока элемент пропадёт: 

# вариант "Должен пропасть"
Poll.for_false { ui.elements_displayed?(locator) }

Но в случае с третьим и четвёртым вариантами всё не так просто.

Рассмотрим вариант «Не должен появиться»: 

# вариант "Не должен появиться"
ui.wait_for_elements_not_displayed(locator)
actual_state = Poll.for(return_on_timeout: true) { ui.elements_displayed?(locator) }
Assertions.assert_false(actual_state, "Element #{locator} should not appear")

Здесь мы, во-первых, фиксируем состояние отсутствия элемента на экране. 

Далее, используя Poll.for с параметром return_on_timeout, мы ждём появления элемента. При этом метод Poll.for не выбросит ошибку, а вернёт false, если элемент не появится. Значение, полученное из Poll.for, сохраняется в переменной actual_state.

После этого происходит проверка неизменности состояния элемента с использованием метода assert.

Для проверки варианта «Не должен пропасть» мы используем похожую логику, ожидая пропажи элемента с экрана вместо его появления: 

# вариант "Не должен пропасть"
ui.wait_for_elements_displayed(locator)
actual_state = Poll.for(return_on_timeout: true) { !ui.elements_displayed?(locator) }
Assertions.assert_false(actual_state, "Element #{locator} should not disappear")

Проверки этих четырёх вариантов изменения состояния актуальны для многих элементов мобильных приложений. А поскольку разработкой тестов у нас занимается много людей, всегда есть вероятность того, что кто-то забудет про некоторые варианты при создании новых проверок. Поэтому мы вынесли реализацию проверок всех вариантов изменения состояния в один метод:

def verify_dynamic_state(state:, timeout: 10, error_message:)
  options = {
    return_on_timeout: true,
    timeout:           timeout,
  }
  case state
    when 'should appear'
      actual_state = Poll.for(options) { yield }
      Assertions.assert_true(actual_state, error_message)
    when 'should disappear'
      actual_state = Poll.for(options) { !yield }
      Assertions.assert_true(actual_state, error_message)
    when 'should not appear'
      actual_state = Poll.for(options) { yield }
      Assertions.assert_false(actual_state, error_message)
    when 'should not disappear'
      actual_state = Poll.for(options) { !yield }
      Assertions.assert_false(actual_state, error_message)
    else
      raise("Undefined state: #{state}")
  end
end

yield – это код блока, переданного в данный метод. На примерах выше это был метод elements_displayed?. Но это может быть любой другой метод, результат выполнения которого отражает состояние необходимого нам элемента. Документация Ruby.

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

Выводы: 

  • важно не забывать про все четыре варианта изменения состояния при проверках UI-элементов;

  • полезно вынести эти проверки в общий метод. 

Мы рекомендуем использовать полную систему проверок всех вариантов изменения состояния. Что мы имеем в виду? Представьте, что когда элемент есть — это состояние true, а когда его нет — false. 

Состояние 1

Состояние 2

Должен появиться

FALSE

TRUE

Должен пропасть

TRUE

FALSE

Не должен появиться

FALSE

FALSE

Не должен пропасть

TRUE

TRUE

Мы строим матрицу всех комбинаций. При появлении нового состояния таблицу можно расширить и получить новые комбинации. 

Практика 5. Надёжная настройка предусловий тестов

Как можно догадаться из заголовка, задача этого раздела — разобраться, как настраивать предусловия перед началом выполнения теста.

Рассмотрим два примера. Первый – отключение сервиса локации на iOS в настройках. Второй – создание истории чата. 

В первом примере реализация метода отключения сервиса локации на iOS выглядит следующим образом: 

def switch_off_location_service
  ui.wait_for_elements_displayed(SWITCH)
  if ui.element_value(SWITCH) == ON
    ui.tap_element(SWITCH)
    ui.tap_element(TURN_OFF)
  end
end

Мы ждём, пока переключатель (элемент switch) появится на экране. Потом проверяем его состояние. Если оно не соответствует ожидаемому, мы его изменяем. 

После этого мы закрываем настройки и запускаем приложение. И иногда внезапно сталкиваемся с проблемой: почему-то сервис локации остаётся включённым. Как это получается? Мы же сделали всё, чтобы его отключить. Кажется, что это проблема работы системных настроек в iOS системе. При быстром выходе из настроек (а тест делает это моментально после нажатия на переключатель) их новое состояние не сохраняется. Но проблемы могут возникнуть и при настройке предусловий в нашем приложении.

Давайте обратимся ко второму примеру — созданию истории чата перед началом выполнения теста. Реализация метода выглядит следующим образом: 

def send_message(from:, to:, message:, count:)
  count.times do
    QaApi.chat_send_message(user_id: from, contact_user_id: to, message: message)
  end
end

Мы используем QAAPI для отправки сообщений по user_id. В цикле мы отправляем необходимое количество сообщений. 

После этого мы заходим в чат и производим необходимые проверки, связанные с загрузкой истории сообщений. Но иногда в чате отображаются не все сообщения. Иногда мы продолжаем получать их, уже находясь в чате, а не до его открытия. Связано это с тем, что сервер не может отправлять нужное количество сообщений моментально. В некоторых случаях доставка задерживается. 

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

Как же решить эту проблему? Мы можем добавить гарантию выполнения действия в методы установки предусловий. 

Тогда наш метод отключения сервиса локации будет выглядеть следующим образом:

def ensure_location_services_switch_in_state_off
  ui.wait_for_elements_displayed(SWITCH)
  if ui.element_value(SWITCH) == ON
    ui.tap_element(SWITCH)
    ui.tap_element(TURN_OFF)
    Poll.for(timeout_message: 'Location Services should be disabled') do
      ui.element_value(SWITCH) == OFF
    end
  end
end

Используя метод Poll.for, мы убеждаемся, что состояние переключателя изменилось, прежде чем переходить к следующим действиям теста. Это позволяет избежать проблем, вызванных тем, что сервис локации время от времени был включён. 

Во втором примере нам снова помогут наши методы QAAPI. 

def send_message(from:, to:, message:, count:)
  actual_messages_count = QaApi.received_messages_count(to, from)
  expected_messages_count = actual_messages_count + count
  count.times do
    QaApi.chat_send_message(user_id: from, contact_user_id: to, message: message)
  end
  QaApi.wait_for_user_received_messages(from, to, expected_messages_count)
end

Перед отправкой сообщений мы получаем текущее количество сообщений в чате, а после — убеждаемся, что необходимое количество сообщений было отправлено. Только после этой проверки тест продолжает своё выполнение. Таким образом, когда мы открываем чат в приложении, мы видим все необходимые сообщения и можем выполнять нужные проверки.

Итак, мы рекомендуем всегда использовать гарантию выполнения действий в процессе установки предусловий в ваших тестах. Это позволит сэкономить время на исследовании падений в тех случаях, когда не срабатывает установка предусловий, потому что тест упадёт сразу же на этапе подготовки. 

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

Более подробно о проблемах, описанных в этом разделе, можно прочитать в статье Мартина Фаулера

Практика 6. Простые и сложные действия, или Независимость шагов в тестах

Простые действия

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

Начнём с теста поиска и отправки GIF-сообщений. 

Сначала нам нужно открыть чат с пользователем, которому мы хотим отправить сообщение: 

When  primary_user opens Chat with chat_user

Потом открыть поле ввода GIF-сообщений: 

And   primary_user switches to GIF input source

Далее нам нужно ввести поисковый запрос для GIF-изображений, убедиться, что список обновился, отправить понравившееся изображение из списка и убедиться, что оно было отправлено.

And   primary_user searches for "bee" GIFs
And   primary_user sends 7th GIF in the list
Then  primary_user verifies that the selected GIF has been sent

Целиком сценарий выглядит так:

Scenario: Searching and sending GIF in Chat
  Given users with following parameters
    | role         | name |
    | primary_user | Dima |
    | chat_user    | Lera |
  And   primary_user logs in
  When  primary_user opens Chat with chat_user
  And   primary_user switches to GIF input source
  And   primary_user searches for "bee" GIFs
  And   primary_user sends 7th GIF in the list
  Then  primary_user verifies that the selected GIF has been sent

Обратим внимание на шаг, который отвечает за поиск гифки: 

And(/^primary_user searches for "(.+)" GIFs$/) do |keyword|
  chat_page = Pages::ChatPage.new.await
  TestData.gif_list = chat_page.gif_list
  chat_page.search_for_gifs(keyword)
  Poll.for_true(timeout_message: 'Gif list is not updated') do
    (TestData.gif_list & chat_page.gif_list).empty?
  end
end

Здесь, как и почти во всех остальных шагах, мы делаем следующее:

  1. сначала ожидаем открытия нужной страницы (ChatPage); 

  2. потом сохраняем список всех доступных GIF-изображений; 

  3. далее вводим ключевое слово для поиска; 

  4. затем ждём изменения состояния — обновления списка (ведь мы говорили о том, что полезно добавлять в тесты проверку выполнения действий).

Кажется, что всё реализовано правильно. После завершения поиска мы убеждаемся, что список изображений обновился, и только после этого отправляем одно из них. Но у нас появится проблема, если мы, например, захотим написать тест, проверяющий, что после ввода идентичного поискового запроса список изображений не обновится. В этом случае нам придётся создавать отдельный шаг для ввода поискового запроса для GIF-изображений, который во многом будет дублировать уже имеющийся.

Подобная проблема возникает в тех случаях, когда какое-то действие может приводить к разным результатам, а мы в шаге фиксируем лишь один из них в виде проверки выполнения действия. Это приводит к сложности переиспользования такого рода шагов, и, следовательно, к замедлению разработки тестов и дубликации кода.

Как же нам этого избежать? Как вы, возможно, заметили, наш шаг поиска GIF-изображений на самом деле включал в себя три действия: 

  1. сохранение текущего списка;

  2. поиск;

  3. проверку обновления списка.

Решением проблемы переиспользования будет разделение этого шага на три простых и независимых. 

Первый шаг сохраняет текущий список изображений:

And(/^primary_user stores the current list of GIFs$/) do
  TestData.gif_list = Pages::ChatPage.new.await.gif_list
end

Второй шаг – поиск гифки — позволяет напечатать ключевое слово для поиска: 

And(/^primary_user searches for "(.+)" GIFs$/) do |keyword|
  Pages::ChatPage.new.await.search_for_gifs(keyword)
end

На третьем шаге мы ждём обновления списка: 

And(/^primary_user verifies that list of GIFs is updated$/) do
  chat_page = Pages::ChatPage.new.await
  Poll.for_true(timeout_message: 'Gif list is not updated') do
    (TestData.gif_list & chat_page.gif_list).empty?
  end
end

В итоге наш первоначальный сценарий выглядит следующим образом:

Scenario: Searching and sending GIF in Chat
  Given users with following parameters
    | role         | name |
    | primary_user | Dima |
    | chat_user    | Lera |
  And   primary_user logs in
  When  primary_user opens Chat with chat_user
  And   primary_user switches to GIF input source
  And   primary_user stores the current list of GIFs
  And   primary_user searches for "bee" GIFs
  Then  primary_user verifies that list of GIFs is updated
  When  primary_user sends 7th GIF in the list
  Then  primary_user verifies that the selected GIF has been sent

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

Но и здесь есть нюансы. Шаги не всегда можно сделать простыми и независимыми. В таких случаях мы будем называть их сложными. 

Сложные действия

Под сложными шагами мы подразумеваем те, которые включают в себя переходы между разными экранами приложения либо работу с изменением состояния какого-либо экрана. Рассмотрим такую ситуацию на примере голосования в мини-игре.

Мини-игра — это экран, на котором пользователю предлагаются профили других людей, которые ему написали. Можно либо отвечать на сообщения, либо пропускать этих пользователей. Действие пропуска назовём «Голосовать “нет”». 

Тестовый пользователь

Тестовый пользователь

Нам необходимо написать тест, который «проголосует “нет”» N раз, закроет экран игры, а потом откроет его снова и проверит, что пользователь находится на правильной позиции.

«Проголосовать “нет”» — простое действие. Но, если мы сделаем для него простой шаг, то для того чтобы проголосовать N раз, нам нужно будет использовать этот шаг столько же раз на уровне сценария. Читать такой сценарий неудобно. Поэтому есть смысл создать более сложный шаг с параметром «Количество голосов», который сможет проголосовать необходимое нам количество раз. 

Кажется, что реализовать такой шаг довольно легко. Необходимо всего лишь проголосовать на странице нужное количество раз. 

When(/^primary_user votes No in Messenger mini game (d+) times$/) do count
  page = Pages::MessengerMiniGamePage.new.await
  count.to_i.times do
    page.vote_no
  end
end

Но тут мы сталкиваемся с проблемой: иногда тест голосует слишком быстро. То есть он нажимает на кнопку до того, как обрабатывается предыдущее нажатие. В этом случае приложение не отправит новый голос на сервер. Наш шаг при этом выполнится успешно. А на следующем шаге, когда мы захотим убедиться, что пользователь находится на правильной позиции в игре, тест упадёт, потому что предыдущий шаг не выполнил свою задачу и сделал меньше голосов, чем было нужно.

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

When(/^primary_user votes No in Messenger mini game (d+) times$/) do count
  page = Pages::MessengerMiniGamePage.new.await
  count.to_i.times do
    progress_before = page.progress
    page.vote_no
    Poll.for_true do
      page.progress > progress_before
    end
  end
end

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

Подведём итоги. Мы создаём независимые шаги для простых действий. Это позволяет легко их переиспользовать и создавать различные сценарии за более короткое время, чем если бы нам нужно было переписывать похожие друг на друга шаги. В шаги для сложных действий мы добавляем проверки того, что действие выполнено, перед переходом к следующим действиям. 

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

Практика 7. Верификация необязательных элементов

Под необязательными элементами мы понимаем такие элементы, которые могут либо отображаться, либо не отображаться на одном и том же экране в зависимости от каких-либо условий. Здесь мы рассмотрим пример диалогов о подтверждении действий пользователя, или алёртов (alerts).

Примеры диалоговых окон

Примеры диалоговых окон

Наверняка вы сталкивались с подобными диалогами в различных мобильных приложениях. В наших двух приложениях, например, их больше 70. Они появляются в разных местах в ответ на разные действия пользователей. Что же представляют собой необязательные элементы на них? 

Проанализируем скриншоты выше.

  • Скриншот 1: заголовок, описание и две кнопки.

  • Скриншот 2: заголовок, описание и одна кнопка.

  • Скриншот 3: описание и две кнопки.

Таким образом, необязательными элементами в этих диалогах являются заголовок и вторая кнопка. Локаторы у элементов на алёртах одинаковые — вне зависимости от того, в каком месте приложения или после какого действия пользователя они появляются. Поэтому мы хотим реализовать проверки для всех типов диалогов один раз на их базовой странице, а потом наследовать от неё каждый конкретный алёрт, чтобы не пришлось повторять этот код. Создание подобных проверок мы и рассмотрим в этом разделе. 

Начнём с того, как выглядит вызов метода для верификации каждого из диалогов:

class ClearAccountAlert < AppAlertAndroid
  def verify_alert_lexemes
    verify_alert(title:        ClearAccount::TITLE,
                 description:  ClearAccount::MESSAGE,
                 first_button: ClearAccount::OK_BUTTON,
                 last_button:  ClearAccount::CANCEL_BUTTON)
  end
end
class WaitForAlert < AppAlertAndroid
  def verify_alert_lexemes
    verify_alert(title:        WaitForTITLE,
                 description:  WaitForMESSAGE,
                 first_button: WaitForCLOSE_BUTTON)
  end
end
class SpecialOffersAlert < AppAlertAndroid
  def verify_alert_lexemes
    verify_alert(description:  SpecialOffers::MESSAGE,
                 first_button: SpecialOffers::SURE_BUTTON,
                 last_button:  SpecialOffers::NO_THANKS_BUTTON)
  end
end

Во всех примерах мы вызываем метод verify_alert, передавая ему лексемы для проверки необходимых элементов. При этом, как вы можете заметить, WaitForAlert мы не передаём лексему для второй кнопки, так как её не должно быть, а SpecialOffersAlert — лексему для заголовка.

Рассмотрим реализацию метода verify_alert:

def verify_alert(title: nil, description:, first_button:, last_button: nil)
  ui.wait_for_elements_displayed([MESSAGE, FIRST_ALERT_BUTTON])
  ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE) if title
  ui.wait_for_element_text(expected_lexeme: description, locator: MESSAGE)
  ui.wait_for_element_text(expected_lexeme: first_button, locator: FIRST_ALERT_BUTTON)
 ui.wait_for_element_text(expected_lexeme: last_button, locator: LAST_ALERT_BUTTON) if last_button
end

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

В чём же проблема этого подхода? В том, что мы пропускаем проверку того, что необязательный элемент не отображается тогда, когда он не должен отображаться. Это может привести к комичным ситуациям. Например, может появиться такой алёрт:

Пользователь не понимает, что выбрать: обе кнопки как будто бы закрывают диалог. Это похоже на критический баг. Даже если приложение не падает, это нужно исправить. А тесты нам нужно изменить так, чтобы они выявляли подобные проблемы.

Для этого в тестах мы меняем проверку 

ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE) if title

на

if title.nil?
  Assertions.assert_false(ui.elements_displayed?(ALERT_TITLE), "Alert title should not be displayed")
else
  ui.wait_for_element_text(expected_lexeme: title, locator: ALERT_TITLE)
end

Мы изменили условие if и добавили проверку второго состояния. Если мы не передаём лексему для необязательного элемента, значит, этого элемента не должно быть на экране, что мы и проверяем. Если же в title есть какой-то текст, мы понимаем, что элемент с этим текстом должен быть, и проверяем его. Мы решили выделить эту логику в общий метод, который назвали wait_for_optional_element_text. Этот метод мы можем применять не только для диалогов из этого примера, но и для любых других экранов приложения, на которых есть необязательные элементы. Видим, что if-условие из примера выше полностью находится внутри нового метода:

def wait_for_optional_element_text(expected_lexeme:, locator:)
  GuardChecks.not_nil(locator, 'Locator should be specified')
  if expected_lexeme.nil?
    Assertions.assert_false(elements_displayed?(locator), "Element with locator #{locator} should not be displayed")
  else
    wait_for_element_text(expected_lexeme: expected_lexeme, locator: locator)
  end
end

Реализация метода verify_alert тоже изменилась: 

def verify_alert(title: nil, description:, first_button:, last_button: nil)
  ui.wait_for_elements_displayed([MESSAGE, FIRST_ALERT_BUTTON])
  ui.wait_for_optional_element_text(expected_lexeme: title, locator: ALERT_TITLE)
  ui.wait_for_element_text(expected_lexeme: description, locator: MESSAGE)
  ui.wait_for_element_text(expected_lexeme: first_button, locator: FIRST_ALERT_BUTTON)
  ui.wait_for_optional_element_text(expected_lexeme: last_button, locator: LAST_ALERT_BUTTON)
end

Мы всё так же ожидаем появления обязательных элементов на экране. После этого проверяем, что их текст совпадает с переданными в метод лексемами. В случае же с необязательными элементами теперь мы используем метод wait_for_optional_element_text, который помогает убедиться, что необязательный элемент не отображается тогда, когда не должен отображаться.  

Подводя итоги, хотим обратить ваше внимание на то, что не бывает необязательных проверок, — бывают необязательные элементы, все состояния которых нужно обязательно проверять. А выделение проверки состояний необязательных элементов в общий метод позволяет нам легко переиспользовать его для разных экранов приложения.

Резюме: мы советуем использовать полную систему проверок всех состояний нужных элементов, а также выделять общие методы для однотипных действий. 

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

Общие рекомендации 

Выделим основные рекомендации по автоматизации тестирования мобильных приложений из семи практик, которые мы описали:

  • так как проверки – это то, ради чего мы пишем тесты, всегда используйте полную систему проверок; 

  • не забывайте добавлять проверку предустановки для асинхронных действий; 

  • выделяйте общие методы для переиспользования однотипного кода — как в шагах, так и в методах на страницах;

  • делайте объект тестирования простым; 

  • выделяйте независимые методы для простых действий в тестах. 

Возможно, эти советы кому-то покажутся очевидными. Но мы хотим обратить ваше внимание на то, что применять их можно (и нужно) в разных ситуациях. Если вы хотите дополнить список другими полезными рекомендациями, добро пожаловать в комментарии! 

Бонус

Мы подготовили тестовый проект, в котором отразили все практики, описанные в статье. Переходите по ссылке, изучайте и применяйте: 

Mobile Automation Sample Project

Сегодня мы поговорим об основных, часто встречающихся ошибках, которые часто допускают разработчики при работе с состоянием (state) при разработке React-приложения.

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

Поскольку мы все учимся, это также означает, что мы все также склонны к ошибкам. Хорошо. Цель — стать лучше и стать лучше. Если ты ошибаешься и учишься на ней, у тебя все отлично! Но если вы не научились чему-то новому и продолжаете повторять одни и те же ошибки, ну … тогда, похоже, вы застаётесь в своей карьере.

В этом духе, вот три распространенных ошибки, которые я часто вижу во время обзоров кода, которые младшие разработчики делают при работе с состоянием компонента React. Мы рассмотрим каждую ошибку и затем обсудим, как ее исправить.

  • Изменение состояния напрямую.

  • При изменении состояния компонента важно, чтобы вы возвращали новую копию состояния с изменениями, а не изменяли текущее состояние напрямую. Если вы неправильно измените состояние компонента, алгоритм сравнения React не поймает изменение, и ваш компонент не будет обновляться должным образом. Давайте посмотрим на пример.

    Скажем, у вас есть состояние, которое выглядит так:

    this.state = {
      colors: ['red', 'green', 'blue']
    }
    

    И теперь вы хотите добавить цвет «yellow» в этот массив. Может быть заманчиво сделать это:

    this.state.colors.push('yellow')
    

    Или даже это:

    this.state.colors = [...this.state.colors, 'yellow']
    

    Но оба эти подхода неверны! При обновлении состояния в компоненте класса вам всегда нужно использовать метод setState, и вы всегда должны быть осторожны, чтобы не изменять объекты. Вот правильный способ добавить элемент в массив:

    this.setState(prevState => ({ colors: [...prevState.colors, 'yellow'] }))
    

    И это приводит нас прямо к ошибке номер два.

  • Установка состояния, которое опирается на предыдущее состояние без использования функции

  • Есть два способа использовать метод setState.

    Первый способ — предоставить объект в качестве аргумента.

    Второй способ — предоставить функцию в качестве аргумента.

    Итак, когда вы хотите использовать один поверх другого?

    Если бы у вас была, например, кнопка, которую можно включить или отключить, у вас может быть элемент состояния под названием isDisabled, который содержит логическое значение. Если вы хотите переключить кнопку с включенного на отключенное, может возникнуть соблазн написать что-то вроде этого, используя объект в качестве аргумента:

    this.setState({ isDisabled: !this.state.isDisabled })
    

    Итак, что с этим не так? Проблема заключается в том, что обновления состояния React могут быть пакетными, а это означает, что в одном цикле обновления может происходить несколько обновлений состояния. Если ваши обновления должны были быть пакетными, и у вас было несколько обновлений до включенного / отключенного состояния, конечный результат может быть не тем, что вы ожидаете.

    Более правильным способом обновления состояния здесь было бы предоставление функции предыдущего состояния в качестве аргумента:

    this.setState(prevState => ({ isDisabled: !prevState.isDisabled }))
    

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

    То же самое верно для чего-то вроде увеличения счетчика.

    Не делайте этого!!!:

    this.setState({ counterValue: this.state.counterValue + 1 })
    

    Делайте правильно!!!:

    this.setState(prevState => ({ counterValue: prevState.counterValue + 1 }))
    

    Ключевым моментом здесь является то, что если ваше новое состояние зависит от значения старого состояния, вы всегда должны использовать функцию в качестве аргумента. Если вы устанавливаете значение, которое не зависит от значения старого состояния, то вы можете использовать объект в качестве аргумента.

  • Не забывайте, что setState является асинхронным

  • Наконец, важно помнить, что setState является асинхронным методом. В качестве примера, давайте представим, что у нас есть компонент с состоянием, которое выглядит так:

    this.state = { name: 'John' }
    

    И затем у нас есть метод, который обновляет состояние и затем выводит состояние на консоль:

    this.setState({ name: 'Matt' })
    console.log(this.state.name)
    

    Вы можете подумать, что вывод будет 'Matt' на консоль, но это не так! Это будет — «Джон»!

    Причина этого в том, что опять же setState является асинхронным. Это означает, что он собирается запустить обновление состояния, когда доберется до строки, которая вызывает setState, но код ниже будет продолжать выполняться, поскольку асинхронный код не является блокирующим.

    Если у вас есть код, который необходимо запустить после обновления состояния, React позволяет предоставить функцию обратного вызова, которая запускается после завершения обновления.

    Правильный способ записать текущее состояние после обновления:

    this.setState({ name: 'Matt' }, () => console.log(this.state.name))
    

    Намного лучше! Теперь вывод в консоль будет 'Matt', как и ожидалось.

    Хотите освоить самые современные методы написания React приложений? Надоели простые проекты? Нужны курсы, книги, руководства, индивидуальные занятия по React и не только? Хотите стать разработчиком полного цикла, освоить стек MERN, или вы только начинаете свой путь в программировании, и не знаете с чего начать, то пишите через форму связи, подписывайтесь на мой канал в Телеге, вступайте в группу на Facebook.

    Простой пример :

    class Element extends Component {
        constructor(props) {
            super(props);
    
            this.state = {
                name: props.name
            }
        }
        render() {
            return <p>{this.state.name}</p>
        }
    }
    
    class Elements extends Component {
        constructor(props) {
            super(props);
    
            this.state = {
                names: props.names
            }
        }
        render() {
            return <div>{
                this.state.names.map(function(name, i) {
                    return <Element key={ i } name={ name } />
                })  
            }</div>
        }
    
        componentWillMount() {
            setTimeout(
                () => {
                    this.state.names.splice(1, 1);
                    let names = this.state.names;
                    this.setState({names: names});
                },
                3000
            );
        }
    }
    
    ReactDOM.render(
        <Elements names={ [0, 1, 2] } />,
        document.body
    );
    

    Вообщем, создаем Element. Он в свойствах принимает name. Пишем name в состояние name. И выводим состояние name.

    Elements принимает в свойства names — массив name для Element. names пишет в свое состояние names. Проходит в выводе состояние names и для каждого создает Element, передавая очередной name в свойства ему.

    Дальше setTimeout — просто для примера, неважно как, но состояние names у Elements меняется. Удаляется какой-либо name из него. В итоге все Element перерисовываются и у каждого нужное свойство name. А вот состояние name не изменяется.

    Стал проверять — да, constructor не срабатывает, когда Element перерисовывается. ComponentWillMount тоже. ComponentWillUnmount срабатывает для одного элемента, но не для того, который удалили, а для последнего почему-то.

    Короче в итоге все выглядит так : Сначала на мониторе 0 1 2 , а затем 0 1 . А должно быть 0 2 .

    Ни как не могу понять, почему так происходит…

    В моем приложении реакции/редукции есть следующие действия

    export function getSeatingChartConfiguration(team) {
     return function(dispatch) {
      ref.child(team.key).once('value').then(function(snapshot) {
        dispatch(loadSeatingChart(snapshot.val()));
      });
     };
    }
    
    export function saveSeatingChartSection(key, sectionData){
     return function(dispatch) {
      ref.child(key).once('value').then(function(snapshot) {
       let data = snapshot.val();
       let sections = data.sections;
       let index = snapshot.val().sections.map( (el) => el.name).indexOf(sectionData.name);
       if(index !== -1) {
        sections[index] = sectionData;
       } else {
        sections.push(sectionData);
       }
       data.sections = sections;
       ref.child(key).update(data, function(error) {
        dispatch(loadSeatingChart(data));
       });
      });
     };
    }
    

    Вот редуктор

    export default function seatingChart(state = {}, action) {
     switch(action.type) {
    
      case actionTypes.LOAD_SEATING_CHART_CONFIGURATION:
       return action.seatingChartConfiguration;
    
      default:
       return state;
     }
    }
    

    Я не получаю никаких ошибок, когда вызывается getSeatingChartConfiguration(), но я получаю Error: A state mutation was detected between dispatches, in the pathseatingChart.sections.2.points.. Что мне нужно изменить в моем действии или редукторе, чтобы не мутировать государство.

    1 ответ

    Используйте Object.assign с начальным состоянием, чтобы избежать изменения состояния, как показано ниже.

    const initialState = { seatingChartConfiguration: [] }
    
    export default function seatingChart(state = initialState, action) {
     switch(action.type) {
    
      case actionTypes.LOAD_SEATING_CHART_CONFIGURATION:
       return Object.assign({}, state, { seatingChartConfiguration: action.seatingChartConfiguration });
    
      default:
       return state;
     }
    }
    


    0

    Elumalai Kaliyaperumal
    5 Окт 2017 в 11:55

    Есть несколько проблем, одна или несколько из которых, вероятно, являются проблемой (особенно №4):

    1. Изменения состояния: асинхронный. Вы не увидите изменений состояния this.state сразу после его вызова. Если вам нужно увидеть изменения, используйте обратный вызов (второй аргумент setState).

    2. SetState не имеет возвращаемого значения, поэтому нет смысла вызывать setState с возвращаемым значением setState. Я бы не подумал, что это проблема, но вот что:

      this.setState(this.setState(res.data));
      

      Должно быть

      this.setState(res.data);
      
    3. В документации не сказано, что метод render может возвращать undefined. Так что это не определено (не каламбур!), Если вы вернете undefined, как вы это делаете.

    4. В вашем render вы используете this.provider. Если provider предоставляется res.data, вы должны использовать this.state.provider.

    Вы упомянули о рассмотрении различных других методов (componentShouldUpdate и т. д.), Но в этом случае нет необходимости. Вы запускаете свой асинхронный процесс в нужном месте (componentDidMount) и устанавливаете состояние в ответ на это завершение (что тоже правильно, хотя см. №1 и №2 выше).

    0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии

    А вот еще интересные материалы:

  • Яшка сломя голову остановился исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного где ошибка
  • Ошибка изменения параметров рабочего сервера кластера
  • Ошибка изменения не могут быть применены код ошибки 1 xigmanas