Русский
Русский
English
Статистика
Реклама

Automation

Перевод Автоматизация обнаружения возможных путей перехвата DLL (DLL Hijacks)

14.08.2020 20:16:22 | Автор: admin
Привет, хабровчане. Прямо сейчас открыт набор на новый поток курса Пентест. Практика тестирования на проникновение. В преддверии старта курса делимся с вами переводом интересного материала.

Введение


В этой статье мы рассмотрим концепцию перехвата порядка поиска динамически подключаемых библиотек (DLL hijacking) и то, как она может быть использована для достижения устойчивости (persistence) в юзерленде в системах Windows. Этот метод описан в MITER ATT&CK в разделе: Перехват порядка поиска DLL (T1038).

Подмена DLL может быть использована злоумышленниками для множества разных целей, но в этой статье основное внимание будет уделено достижению устойчивости с помощью приложений с автозапуском. Например, поскольку Slack и Microsoft Teams запускаются при загрузке (по умолчанию), подмена DLL в одном из этих приложений позволит злоумышленнику получить устойчивый доступ к своей цели всякий раз, когда пользователь входит в систему.

После представления концепции DLL, порядка поиска DLL, и подмены DLL, я раскрою процесс автоматизации обнаружения возможности перехвата DLL. В этой статье будет рассказано об обнаружении путей перехвата DLL в Slack, Microsoft Teams и Visual Studio Code.

Наконец, я обнаружил несколько путей перехвата DLL, которые использовались разными приложениями, исследовал основную причину и обнаружил, что приложения, использующие определенные вызовы API Windows, подвержены перехвату DLL, когда не работают из C:\Windows\System32\.

Я хочу поблагодарить своего коллегу Джозайю Массари (@Airzero24) за то, что он первым обнаружил некоторые из этих перехватов DLL, объяснил их методологию и вдохновил меня автоматизировать обнаружение.

Что такое DLL?


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

Функциональные возможности DLL могут быть использованы приложением Windows с помощью одной из функций LoadLibrary*. Приложения могут ссылаться на DLL, разработанные специально для этих приложений, или DLL Windows, уже находящиеся на диске в System32. Разработчики могут загружать DLL из System32, чтобы использовать в своих приложениях функционал, уже реализованный в Windows, без необходимости писать этот функционал с нуля.

Например, разработчик, которому необходимо выполнять HTTP-запросы, может использовать библиотеку WinHTTP (winhttp.dll) вместо реализации HTTP-запросов с использованием сырых сокетов.

Порядок поиска DLL и перехват


Поскольку DLL существуют в виде файлов на диске, вы можете задаться вопросом, как приложение узнает, откуда загружать DLL? Microsoft подробно задокументировала порядок поиска DLL здесь.

Начиная с Windows XP SP2, безопасный режим поиска DLL включен по умолчанию (HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode). При включенном безопасном режиме порядок поиска DLL следующий:

  1. Каталог, из которого загружено приложение.
  2. Системный каталог. Используйте функцию GetSystemDirectory, чтобы получить путь к этому каталогу.
  3. 16-битный системный каталог. Функции, которая предоставляет путь к этому каталогу, нет, но в нем происходит поиск.
  4. Каталог Windows. Используйте функцию GetWindowsDirectory, чтобы получить путь к этому каталогу.
  5. Текущий каталог.
  6. Каталоги, перечисленные в переменной среды PATH. Обратите внимание, что сюда не входят пути для каждого приложения, указанные в разделе реестра App Paths. Ключ App Paths не используется при вычислении пути поиска DLL.

Система может содержать несколько версий одной и той же DLL. Приложения могут управлять выбором расположения, из которого должен загружается DLL, путем указания полного пути или использования другого механизма, такого как манифест. (Сурс)

Если приложение не указывает, откуда загружать DLL, Windows использует порядок поиска DLL по умолчанию, приведенный выше. Первая позиция в порядке поиска DLL (каталог, из которого загружается приложение) представляет интерес для злоумышленников.

Если разработчик приложения предполагает загрузку DLL из C:\Windows\System32, но явно не прописал это в приложении, вредоносная DLL, помещенная в каталог приложения, будет загружена раньше, чем легитимная DLL из System32. Загрузка вредоносной DLL называется подменой (или перехватом) DLL и используется злоумышленниками для загрузки вредоносного кода в доверенные/подписанные приложения.

Использование подмены DLL для достижения устойчивости


Подмена DLL может использоваться для достижения устойчивости, когда уязвимое приложение/служба запускается и вредоносная DLL размещается в уязвимом месте. Мой коллега, @Airzero24, обнаружил подмену DLL в Microsoft OneDrive, Microsoft Teams и Slack в виде userenv.dll.

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


Приложения Windows, настроенные на автозапуск

Чтобы проверить подмену DLL, я создал загрузчик шеллкода DLL, который запускал Cobalt Strike Beacon. Я переименовал вредоносную DLL в userenv.dll и скопировал ее в каталог уязвимого приложения. Я запустил приложение и увидел свой новый Beacon коллбек.


Cobalt Strike Beacon через перехват DLL

Используя Process Explorer, я могу проверить, действительно ли моя вредоносная DLL была загружена уязвимым приложением.


Обозреватель процессов, показывающий загруженную вредоносную DLL

Автоматическое обнаружение возможности перехвата DLL


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

Код, использованный в моих проверка, можно найти здесь.

На примере Slack


Чтобы начать этот процесс, я запустил Process Monitor (ProcMon) со следующими фильтрами:

  • Имя процесса slack.exe
  • Результат содержит NOT FOUND
  • Путь заканчивается на .dll.


Поиск отсутствующих DLL в ProcMon.

Затем я запустил Slack и изучил ProcMon на предмет любых DLL, которые Slack искал, но не смог найти.


Возможные пути перехвата DLL, обнаруженные с помощью ProcMon

Я экспортировал эти данные из ProcMon в виде CSV файла для облегчения парсинга в PowerShell.

С моей текущей DLL загрузчика шелл-кода я не смог бы легко определить имена DLL, которые были успешно загружены Slack. Я создал новую DLL, которая использовала GetModuleHandleEx и GetModuleFileName для определения имени загруженной DLL и записи его в текстовый файл.

Моя следующая цель состояла в том, чтобы проанализировать CSV-файл на наличие в списке путей к DLL, просмотреть этот список, скопировать мою тестовую DLL по указанному пути, запустить целевой процесс, остановить целевой процесс и удалить тестовую DLL. Если тестовая DLL была успешно загружена, она запишет свое имя в результирующий файл.

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

Всю магию в моем проекте DLLHijackTest творит скрипт PowerShell. Он принимает путь к CSV-файлу, сгенерированному ProcMon, путь к вашей вредоносной DLL, путь к процессу, который вы хотите запустить, и любые аргументы, которые вы хотите передать процессу.


Параметры Get-PotentialDLLHijack


Get-PotentialDLLHijack.ps1

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

PS C:Users\John\Desktop> Get-PotentialDLLHijack -CSVPath .\Logfile.CSV -MaliciousDLLPath .\DLLHijackTest.dll -ProcessPath "C:\Users\John\AppData\Local\slack\slack.exe"C:\Users\John\AppData\Local\slack\app-4.6.0\WINSTA.dllC:\Users\John\AppData\Local\slack\app-4.6.0\LINKINFO.dllC:\Users\John\AppData\Local\slack\app-4.6.0\ntshrui.dllC:\Users\John\AppData\Local\slack\app-4.6.0\srvcli.dllC:\Users\John\AppData\Local\slack\app-4.6.0\cscapi.dllC:\Users\John\AppData\Local\slack\app-4.6.0\KBDUS.DLL

На примере Microsoft Teams


Выполняем описанный выше процесс еще раз:

  1. Используйте ProcMon для выявления потенциальных путей перехвата DLL, экспортируйте эти данные в виде CSV файла.
  2. Определите путь запуска процесса.
  3. Определите любые аргументы, которые вы хотите передать процессу.
  4. Запустите Get-PotentialDLLHijack.ps1 с соответствующими аргументами.

Я обнаружил следующие возможные пути перехвата для Microsoft Teams:

PS C:Users\John\Desktop> Get-PotentialDLLHijack -CSVPath .\Logfile.CSV -MaliciousDLLPath .\DLLHijackTest.dll -ProcessPath "C:\Users\John\AppData\Local\Microsoft\Teams\Update.exe" -ProcessArguments '--processStart "Teams.exe"'C:\Users\John\AppData\Local\Microsoft\Teams\current\WINSTA.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\LINKINFO.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\ntshrui.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\srvcli.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\cscapi.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\WindowsCodecs.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\TextInputFramework.dll

Примечание: Мне пришлось внести небольшие изменения в скрипт PowerShell, чтобы завершить Teams.exe, так как мой скрипт пытается завершить процесс, который он пытался запустить, в данном случае это Update.exe.

На примере Visual Studio Code


Повторяя описанный выше процесс, я обнаружил следующие потенциальные пути перехвата для Visual Studio Code:

PS C:Users\John\Desktop> Get-PotentialDLLHijack -CSVPath .\Logfile.CSV -MaliciousDLLPath .\DLLHijackTest.dll -ProcessPath "C:\Users\John\AppData\Local\Programs\Microsoft VS Code\Code.exe"C:\Users\John\AppData\Local\Programs\Microsoft VS Code\WINSTA.dllC:\Users\John\AppData\Local\Programs\Microsoft VS Code\LINKINFO.dllC:\Users\John\AppData\Local\Programs\Microsoft VS Code\ntshrui.dllC:\Users\John\AppData\Local\Programs\Microsoft VS Code\srvcli.dllC:\Users\John\AppData\Local\Programs\Microsoft VS Code\cscapi.dll

Подмена общих (shared) DLL


Я заметил, что Slack, Microsoft Teams и Visual Studio Code совместно используют следующие DLL:

  • WINSTA.dll
  • LINKINFO.dll
  • ntshrui.dll
  • srvcli.dll
  • cscapi.dll

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

Методология: понимание путей перехвата общих DLL


Я следил за стек трейсом, когда Slack пытался загрузить WINSTA.dll, LINKINFO.dll, ntshrui.dll, srvcli.dll и cscapi.dll.

DLL с отложенной загрузкой

Я заметил сходство в стек трейсе при загрузке WINSTA.dll, LINKINFO.dll, ntshrui.dll и srvcli.dll.


Стек трейс, когда Code.exe пытается загрузить WINSTA.dll


Стек трейс, когда Teams.exe пытается загрузить LINKINFO.dll,


Стек трейс, когда Slack пытается загрузить ntshrui.dll

Стек трейс постоянно содержит вызов _tailMerge_<dllname>_dll, delayLoadHelper2 за которым следует LdrResolveDelayLoadedAPI. Такое поведение было одинаковым для всех трех приложений.

Я определил, что это поведение связано с отложенной загрузкой DLL. Из стек трейса при загрузке WINSTA.dll я мог видеть, что модулем, ответственным за эту отложенную загрузку, был wtsapi32.dll.

Я открыл wtsapi32.dll в Ghidra и использовал Search -> For Strings -> Filter: WINSTA.dll. Двойной клик по найденной строке приведет вас к ее локации в памяти.


Строка WINSTA.dll в wtsapi32.dll

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


Ссылки на WINSTA.dll

Следуя ссылкам, мы видим, что строка WINSTA.dll передается в структуру с именем ImgDelayDescr. Глядя на документацию по этой структуре, мы можем подтвердить, что она связана с отложенной загрузкой DLL.

typedef struct ImgDelayDescr {   DWORD        grAttrs;        // атрибуты   RVA          rvaDLLName;     // RVA в имя dll    RVA          rvaHmod;        // RVA дескриптора модуля   RVA          rvaIAT;         // RVA IAT   RVA          rvaINT;         // RVA INT   RVA          rvaBoundIAT;    // RVA опциональной привязки IAT   RVA          rvaUnloadIAT;   // RVA опциональной копии оригинального IAT   DWORD        dwTimeStamp;    // 0, если не привязано,                                // O.W. дата/таймстемп DLL, привязанной к (Old BIND)   } ImgDelayDescr, * PImgDelayDescr;

Эту структуру можно передать в __delayLoadHelper2, который будет использовать LoadLibrary/GetProcAddress для загрузки указанной DLL и исправления адреса импортируемой функции в таблице адресов импорта отложенной загрузки (IAT).

FARPROC WINAPI __delayLoadHelper2(   PCImgDelayDescr pidd,  // Константный указатель на структуру ImgDelayDescr   FARPROC * ppfnIATEntry // Указатель на слот в IAT отложенной загрузки);

Найдя другие ссылки на нашу структуру ImgDelayDescr, мы можем найти вызов __delayLoadHelper2, который затем вызывает ResolveDelayLoadedAPI. Я переименовал имя функции, типы и переменные, чтобы облегчить понимание.


__delayLoadHelper2 и ResolveDelayLoadedAPI в Ghidra

Отлично! Это соответствует тому, что мы видели в нашем ProcMon стек трейсе, когда Slack пытался загрузить WINSTA.dll.


__delayLoadHelper2 и ResolveDelayLoadedAPI в ProcMon.

Такое поведение было единообразно для WINSTA.dll, LINKINFO.dll, ntshrui.dll и srvcli.dll. Основным отличием каждой DLL с отложенной загрузкой была родительская DLL. Во всех трех приложениях:

  • wtsapi32.dll отложено загружала WINSTA.dll
  • shell32.dll отложенно загружала LINKINFO.dll
  • LINKINFO.dll отложено загружала ntshrui.dll
  • ntshrui.dll отложено загружала srvcli.dll

Заметили что-нибудь интересное? Похоже, что shell32.dll загружает LINKINFO.dll, который загружает ntshrui.dll, который, наконец, загружает srvcli.dll. Это подводит нас к нашему последнему общему потенциальному варианту подмены DLL cscapi.dll.

Подмена DLL в NetShareGetInfo и NetShareEnum


Я следил за стек трейсом, когда Slack пытался загрузить cscapi.dll и видел вызов LoadLibraryExW, который, по-видимому, исходил из srvcli.dll.


Стек трейс при загрузке cscapi.dll

Я открыл srvcli.dll в Ghidra и использовал Search -> For Strings -> Filter: cscapi.dll. Двойной клик по найденной строке и переход по ссылкам приводит к ожидаемому LoadLibrary вызову.


srvcli.dll вызывает LoadLibrary для cscapi.dll

Переименовав функцию, содержащую вызов LoadLibrary, и проследовав по ссылкам, я получил два места использования функции:



NetShareEnum загружает cscapi.dll


NetShareGetInfo загружает cscapi.dll

Я проверил это с помощью РоС программ, которые вызывали NetShareEnum и NetShareGetInfo:


NetShareEnum.exe загружает cscapi.dll


NetShareGetInfo.exe загружает cscapi.dll

Результаты


В Slack доступны следующие пути подмены DLL:

C:\Users\John\AppData\Local\slack\app-4.6.0\WINSTA.dllC:\Users\John\AppData\Local\slack\app-4.6.0\LINKINFO.dllC:\Users\John\AppData\Local\slack\app-4.6.0\ntshrui.dllC:\Users\John\AppData\Local\slack\app-4.6.0\srvcli.dllC:\Users\John\AppData\Local\slack\app-4.6.0\cscapi.dllC:\Users\John\AppData\Local\slack\app-4.6.0\KBDUS.DLL

В Microsoft Teams доступны следующие пути подмены DLL:

C:\Users\John\AppData\Local\Microsoft\Teams\current\WINSTA.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\LINKINFO.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\ntshrui.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\srvcli.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\cscapi.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\WindowsCodecs.dllC:\Users\John\AppData\Local\Microsoft\Teams\current\TextInputFramework.dll

В Visual Studio Code доступны следующие пути подмены DLL:

C:\Users\John\AppData\Local\Programs\Microsoft VS Code\WINSTA.dllC:\Users\John\AppData\Local\Programs\Microsoft VS Code\LINKINFO.dllC:\Users\John\AppData\Local\Programs\Microsoft VS Code\ntshrui.dllC:\Users\John\AppData\Local\Programs\Microsoft VS Code\srvcli.dllC:\Users\John\AppData\Local\Programs\Microsoft VS Code\cscapi.dll

Кроме того, я обнаружил, что программы, использующие NetShareEnum и NetShareGetInfo, предоставляют возможность подмены DLL в форме cscapi.dll из-за жестко захардкоженного вызова LoadLibrary. Я подтвердил это поведение с помощью Ghidra и PoC.

Заключение


Напомним, что перехват DLL это метод, с помощью которого злоумышленники могут повлиять на выполнение кода в подписанных/доверенных приложениях. Я создал инструменты, помогающие автоматизировать обнаружение путей перехвата DLL. Используя этот инструмент, я обнаружил пути перехвата DLL в Slack, Microsoft Teams и Visual Studio Code.

Я заметил, что пути перехвата DLL этих трех приложений частично совпадают, и исследовал причину. Я осветил свою методику понимания данного совпадения. Я узнал о DLL с отложенной загрузкой и обнаружил два вызова API, которые делают возможным перехват DLL в любой программе, которая их вызывает:

  • NetShareEnum загружает cscapi.dll
  • NetShareGetInfo загружает cscapi.dll

Спасибо, что нашли время, чтобы прочитать эту статью, надеюсь, вы узнали кое-что новое о Windows API, Ghidra, ProcMon, DLL и перехвате DLL!

Ссылки


Большой привет моим коллегам Дэниелу Хейнсену (@hotnops), Ли Кристенсену (@tifkin_) и Мэтту Хэнду(@matterpreter) за то, что они помогли справиться с Ghidra/ProcMon!



Проверка публичных PoC для использования при пентесте



Читать ещё:


Подробнее..

PyOpenRPA туториал. Управление WEB приложениями

16.08.2020 12:14:15 | Автор: admin

Долгожданный туториал по управлению сторонними WEB приложениями с помощью pyOpenRPA. Во 2-й части мы разберем принципы роботизированного воздействия на HTML/JS. А также своими руками сделаем небольшого, но очень показательного робота.


Этот робот будет полезен тем, для кого актуальна тема покупки/продажи недвижимости.


pyOpenRPA туториал. Управление WEB приложениями


Для тех, кто с нами впервые


pyOpenRPA это open source RPA платформа, которая в полной мере позволяет заменить топовые коммерческие RPA платформы.


Подробнее про то, чем же она полезна, можно почитать здесь.


Навигация по туториалам pyOpenRPA


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


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


Перечень статей-туториалов (опубликованные и планируемые):



А теперь перейдем к самому туториалу.


Немного теории и терминов


[Из википедии]


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


Веб приложения стали широко использоваться в конце 1990-х начале 2000-х годов.


Ссылка на источник


Ок, с выдержкой из вики все #КрутоУмно, но от этого не легче (для тех, кто в этой теме дилетант). Продемонстрирую устройство WEB приложения на примере "Что видим мы?"/"Что видит робот?". Для этого отправимся на сайт одной известной WEB площадки по объявлениям по недвижимости


Что видим мы?


Мы видим красиво сверстанный сайт с интуитивно понятным интерфейсом, на котором можно найти интересные объявления о продаже/сдаче в аренду недвижимости.


Как мы видим WEB приложение


Что видит робот?


Робот видит огромную гипертекстовую разметку HTML с примесью алгоритмического кода JS и завернутого в каскадную таблицу стилей CSS. Увлекательно, правда? :)


Как робот видит WEB приложение


Интерпретация


WEB приложения это один из самых легко роботизируемых классов приложений. Обилие инструментов + технологий позволяют реализовывать практически любую поставленную задачу в кооперации с ними.


Управлять WEB страницей можно с помощью разных технологий адресации: CSS, XPath, id, class, attribute. Мы будем взаимодействовать со страницей с помощью CSS селекторов.


(По шагам) робот своими руками


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


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


{    "SearchKeyStr": "МСК_Тверской", # Ключевое слово поиска    "SearchTitleStr": "Москва, район Тверской", # Заголовок поиска    "SearchURLStr": "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=37.6166_55.7678%2C37.6147_55.7688%2C37.6114_55.7694%2C37.6085_55.7698%2C37.6057_55.77%2C37.6018_55.77%2C37.5987_55.77%2C37.5961_55.7688%2C37.5942_55.7677%2C37.5928_55.7663%2C37.5915_55.7647%2C37.5908_55.7631%2C37.5907_55.7616%2C37.5909_55.7595%2C37.5922_55.7577%2C37.5944_55.7563%2C37.5968_55.7555%2C37.6003_55.7547%2C37.603_55.7543%2C37.6055_55.7542%2C37.6087_55.7541%2C37.6113_55.7548%2C37.6135_55.756%2C37.6151_55.7574%2C37.6163_55.7589%2C37.6179_55.7606%2C37.6187_55.7621%2C37.619_55.7637%2C37.6194_55.7651%2C37.6193_55.7667%2C37.6178_55.7679%2C37.6153_55.7683%2C37.6166_55.7678&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1", # URL of the CIAN search [str]    "SearchDatetimeStr": "2020-08-01 09:33:00.838081", # Дата, на которую была сформирована выгрузка    "SearchItems": { # Перечень извлеченных ценовых объявлений        "https://www.cian.ru/sale/flat/219924574/:": { # URL ссылка на ценовое объявление            "TitleStr": "3-комн. кв., 31,4 м, 5/8 этаж", # Заголовок ценового объявления            "PriceFloat": 10000000.0, # Стоимость общая            "PriceSqmFloat": 133333.0, # Стоимость на 1 кв. м.            "SqMFloat": 31.4, # Кол-во кв. м.            "FloorCurrentInt": 5, # Этаж лота по объявлению            "FloorTotalInt": 8, # Этажей в доме всего            "RoomCountInt": 3 # Кол-во комнат        }    }}

Шаг 0. Подготовим проект для нового робота (развернем pyOpenRPA)


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


Доступно несколько вариантов загрузки pyOpenRPA:


  • Вариант 1, простой. Скачать преднастроенную портативную версию с GitLab страницы проекта
  • Вариант 2, сложный. Установить pyOpenRPA в свою версию интерпретатора Python 3 (pip install pyOpenRPA)

Я рекомендую воспользоваться простым вариантом (вариант 1). Преднастроенная версия не требуется каких-либо настроек инфраструктуры. Здесь в лучших традициях pyOpenRPA реализован принцип, когда пользователь скачивает репозиторий, и у него уже все настроено из коробки пользователю остается лишь писать скрипт робота. #Enjoy :)


Шаг 1. Создать проект робота


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


Ниже приведу зависимости проекта от сторонних компонентов:


  • Selenium WebDriver
  • Google Chrome или Mozilla Firefox или Internet Explorer
  • Python 3

Если вы пошли по варианту 1 (см. шаг 0), то у Вас все эти компоненты уже будут развернуты и настроены внутри скачанного репозитория pyOpenRPA (#Удобно). Репозиторий pyOpenRPA уже содержит все необходимые portable версии требуемых программ (Google Chrome, Mozilla Firefox, Python3 32|64 и т.д.).


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


Создадим следующую структуру проекта:


  • Репозиторий pyOpenRPA > Wiki > RUS_Tutorial > WebGUI_Habr:
    • Файл "3. MonitoringCIAN_Run_64.py" скрипт робота, который мониторит WEB площадку
    • Файл "3. MonitoringCIAN_Run_64.cmd" скрипт запуска робота с 1-го клика по аналогии с .exe файлами

Ниже приведу пример "3. MonitoringCIAN_Run_64.cmd" файла:


cd %~dp0..\..\..\Sources..\Resources\WPy64-3720\python-3.7.2.amd64\python.exe "..\Wiki\RUS_Tutorial\WebGUI_Habr\3. MonitoringCIAN_Run_64.py"pause >nul

Для инициализации Selenium WebDriver воспользуемся следующей функцией:


########################### Init the Chrome web driver###########################def WebDriverInit(inWebDriverFullPath, inChromeExeFullPath, inExtensionFullPathList):    # Set full path to exe of the chrome    lWebDriverChromeOptionsInstance = webdriver.ChromeOptions()    lWebDriverChromeOptionsInstance.binary_location = inChromeExeFullPath    # Add extensions    for lExtensionItemFullPath in inExtensionFullPathList:        lWebDriverChromeOptionsInstance.add_extension (lExtensionItemFullPath)    # Run chrome instance    lWebDriverInstance = None    if inWebDriverFullPath:        # Run with specified web driver path        lWebDriverInstance = webdriver.Chrome(executable_path = inWebDriverFullPath, options=lWebDriverChromeOptionsInstance)    else:        lWebDriverInstance = webdriver.Chrome(options = lWebDriverChromeOptionsInstance)    # Return the result    return lWebDriverInstance

Шаг 2. Запустить WEB инструменты разработчика и сформировать CSS селекторы


В нашем случае WEB инструменты разработчика мы будем использовать из Google Chrome, который предустановлен в репозитории pyOpenRPA (вариант 1 из шага 0).


Откроем Google Chrome и инструменты разработчика (pyOpenRPA repo\Resources\GoogleChromePortable\App\Chrome-bin\chrome.exe, после чего Ctrl + Shift + i)
Portable Google Chrome + Dev Tools


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


Пример поискового запроса


Список ценовых предложений по фильтру


Для того, чтобы подобрать CSS селектор нам помогут инструменты разработчика Google Chrome. Подробнее узнать про устройство CSS селекторов можно здесь по ссылке
Для проверки правильности CSS селектора я буду делать следующую проверку в инструментах разработчика на вкладке "Console". На картинке представлен пример того, как проводится проверки правильности CSS селектора для извлечения списка ценовых предложений.


Пример проверки CSS селектора


Подберем CSS селектор для выборки списка ценовых предложений на странице.


При составлении селектора выяснилось, что в список объявлений встроены рекламные баннеры, которые не содержат информацию о ценовом предложении.


И таких видов рекламных баннеров было обнаружено несколько видов:


  • div[data-name="BannerServicePlaceInternal"]
  • div[data-name="getBannerMarkup"]
  • div[data-name="AdFoxBannerTracker"]

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


  • CSS селектор, Список ценовых предложений: div[data-name="Offers"] > div:not([data-name="BannerServicePlaceInternal"]):not([data-name="getBannerMarkup"]):not([data-name="AdFoxBannerTracker"])

Подберем CSS селекторы по извлечению параметров ценового предложения: Заголовок, Стоимость общая, URL ссылка на карточку.


  • CSS селектор, Заголовок: div[data-name="TopTitle"],div[data-name="Title"]
  • CSS селектор, Стоимость общая: div[data-name="Price"] > div[class="header"],div[data-name="TopPrice"] > div[class="header"]
  • CSS селектор, URL ссылка на карточку: a[class*="--header--"]

Подберем CSS селектор для извлечения кнопки на следующую страницу.


  • CSS селектор, Указатель на следующую страницу: div[data-name="Pagination"] li[class*="active"] + li a

Шаг 3. Обработать/преобразовать получаемые данные


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


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


lOfferItemInfo = { # Item URL with https    "TitleStr": "3-комн. кв., 31,4 м, 5/8 этаж", # Offer title [str]    "PriceFloat": 10000000.0, # Price [float]    "PriceSqmFloat": 133333.0, # CALCULATED Price per square meters [float]    "SqMFloat": 31.4, # Square meters in flat [float]    "FloorCurrentInt": 5, # Current floor [int]    "FloorTotalInt": 8, # Current floor [int]    "RoomCountInt": 3  # Room couint [int]}

Для начала получим список элементов ценовых предложений.


lOfferListCSSStr = 'div[data-name="Offers"] > div:not([data-name="BannerServicePlaceInternal"]):not([data-name="getBannerMarkup"]):not([data-name="AdFoxBannerTracker"])'lOfferList = inWebDriver.find_elements_by_css_selector(css_selector=lOfferListCSSStr)

Далее циклическая обработка каждого ценового предложения.


for lOfferItem in lOfferList:

Извлечем параметры из WEB страницы: Заголовок, Стоимость общая, URL на карточку.


lTitleStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="TopTitle"],div[data-name="Title"]').text # Extract title textlPriceStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="Price"]  > div[class*="header"],div[data-name="TopPrice"] > div[class*="header"]').text # Extract total pricelURLStr = lOfferItem.find_element_by_css_selector(css_selector='a[class*="--header--"]').get_attribute("href") # Extract offer URLlOfferItemInfo["TitleStr"] = lTitleStr # set the titlelPriceStr = lPriceStr.replace(" ","").replace("","") # Remove some extra symbolslOfferItemInfo["PriceFloat"] = round(float(lPriceStr),2) # Convert price to the float type

Извлечем недостающие параметры алгоритмическим путем.


  • Если в заголовке содержится слово "Апартаменты"


    lREResult = re.search(r".*, (\d*,?\d*) м, (\d*)/(\d*) эта.", lTitleStr)  # run the relOfferItemInfo["RoomCountInt"] = 1 # Room countlSqmStr = lREResult.group(1)lSqmStr= lSqmStr.replace(",",".")lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm countlOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(2)) # Floor currentlOfferItemInfo["FloorTotalInt"] = int(lREResult.group(3)) # Floor totallOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M
    

  • Если в заголовке не содержится слово "Апартаменты"


    lREResult = re.search(r".*(\d)-комн. .*, (\d*,?\d*) м, (\d*)/(\d*) эта.", lTitleStr) # run the relOfferItemInfo["RoomCountInt"] = int(lREResult.group(1)) # Room countlSqmStr = lREResult.group(2)lSqmStr= lSqmStr.replace(",",".")lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm countlOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(3)) # Floor currentlOfferItemInfo["FloorTotalInt"] = int(lREResult.group(4)) # Floor totallOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M
    


В примере выше применяется магия регулярных выражений


Для подбора правильных регулярных выражений я пользуюсь online валидаторами типа таких


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


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


inWebDriver.execute_script("""document.querySelector('div[data-name="Pagination"] li[class*="active"] + li a').click()""")

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


# wait while preloader is activelDoWaitBool = Truewhile lDoWaitBool:    lPreloaderCSS = inWebDriver.find_elements_by_css_selector(css_selector='div[class*="--preloadOverlay--"]') # So hard to catch the element :)    if len(lPreloaderCSS)>0: time.sleep(0.5) # preloader is here - wait    else: lDoWaitBool = False # Stop wait if preloader is dissappear

Итоговую структуру сохраним в .json файл.


# Check dir - create if not existsif not os.path.exists(os.path.join('Datasets',lResult['SearchKeyStr'])):    os.makedirs(os.path.join('Datasets',lResult['SearchKeyStr']))# Save result in filelFile = open(f"{os.path.join('Datasets',lResult['SearchKeyStr'],lDatetimeNowStr.replace(' ','_').replace('-','_').replace(':','_').replace('.','_'))}.json","w",encoding="utf-8")lFile.write(json.dumps(lResult))lFile.close()

Шаг 4. Обработка нештатных ситуаций


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


  • Зависает ползунок загрузки при переключении на сл. страницу
  • При переключении на следующую страницу открывается совсем не следующая страница (иногда, но случалось :) )

Но роботы не боятся таких проблем (на то они и роботы :) ).


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


  • Зависает ползунок загрузки при переключении на сл. страницу


    # wait while preloader is active. If timeout - retry all joblTimeFromFLoat = time.time() # get current time in float (seconds)lDoWaitBool = Truewhile lDoWaitBool:lPreloaderCSS = inWebDriver.find_elements_by_css_selector(css_selector='div[class*="--preloadOverlay--"]')if len(lPreloaderCSS)>0: time.sleep(0.5) # preloader is here - waitelse: lDoWaitBool = False # Stop wait if preloader is dissappearif (time.time() - lTimeFromFLoat) > 15: # check if timeout is more than 15 seconds    lRetryJobBool = True # Loading error on page - do break, then retry the job    if inLogger: inLogger.warning(f"Ожидание загрузки страницы более {15} с., Робот повторит задание сначала")    break # break the loopif lRetryJobBool == True: # break the loop if RetryJobBool is truebreak
    

  • При переключении на следующую страницу открывается совсем не следующая страница (иногда, но случалось :) )


    lPageNumberInt = int(inWebDriver.find_element_by_css_selector(css_selector='li[class*="--active--"] span').text) # Get the current page int from web and check with iterator (if not equal - retry all job)if lPageNumberInt == lPageCounterInt:... Код робота ...else:lRetryJobBool = Trueif inLogger: inLogger.warning(    f"Следующая страница по списку не была загружена. Была загружена страница: {lPageNumberInt}, Ожидалась страница: {lPageCounterInt}")
    


Шаг 5. Консолидировать код в проекте робота


Соберем все блоки воедино.


Получим следующий пакет (открыть на GitLab):


# Init Chrome web driver with extensions (if applicable)# Import sectionfrom selenium import webdriverimport timeimport re # Regexp to extract info from stringimport jsonimport datetimeimport osimport reimport copyimport logging# Store structure (.json)"""{    "SearchKeyStr": "МСК_Тверской",    "SearchTitleStr": "Москва, район Тверской", # Title of the search [str]    "SearchURLStr": "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=37.6166_55.7678%2C37.6147_55.7688%2C37.6114_55.7694%2C37.6085_55.7698%2C37.6057_55.77%2C37.6018_55.77%2C37.5987_55.77%2C37.5961_55.7688%2C37.5942_55.7677%2C37.5928_55.7663%2C37.5915_55.7647%2C37.5908_55.7631%2C37.5907_55.7616%2C37.5909_55.7595%2C37.5922_55.7577%2C37.5944_55.7563%2C37.5968_55.7555%2C37.6003_55.7547%2C37.603_55.7543%2C37.6055_55.7542%2C37.6087_55.7541%2C37.6113_55.7548%2C37.6135_55.756%2C37.6151_55.7574%2C37.6163_55.7589%2C37.6179_55.7606%2C37.6187_55.7621%2C37.619_55.7637%2C37.6194_55.7651%2C37.6193_55.7667%2C37.6178_55.7679%2C37.6153_55.7683%2C37.6166_55.7678&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1", # URL of the CIAN search [str]    "SearchDatetimeStr": "2020-08-01 09:33:00.838081", # Date of data extraction,  [str]    "SearchItems": {        "https://www.cian.ru/sale/flat/219924574/:": { # Item URL with https            "TitleStr": "3-комн. кв., 31,4 м, 5/8 этаж", # Offer title [str]            "PriceFloat": 10000000.0, # Price [float]            "PriceSqmFloat": 133333.0, # CALCULATED Price per square meters [float]            "SqMFloat": 31.4, # Square meters in flat [float]            "FloorCurrentInt": 5, # Current floor [int]            "FloorTotalInt": 8, # Current floor [int]            "RoomCountInt": 3 # Room couint [int]        }    }}"""########################### Init the Chrome web driver###########################gChromeExeFullPath = r'..\Resources\GoogleChromePortable\App\Chrome-bin\chrome.exe'gExtensionFullPathList = []gWebDriverFullPath = r'..\Resources\SeleniumWebDrivers\Chrome\chromedriver_win32 v84.0.4147.30\chromedriver.exe'def WebDriverInit(inWebDriverFullPath, inChromeExeFullPath, inExtensionFullPathList):    # Set full path to exe of the chrome    lWebDriverChromeOptionsInstance = webdriver.ChromeOptions()    lWebDriverChromeOptionsInstance.binary_location = inChromeExeFullPath    # Add extensions    for lExtensionItemFullPath in inExtensionFullPathList:        lWebDriverChromeOptionsInstance.add_extension (lExtensionItemFullPath)    # Run chrome instance    lWebDriverInstance = None    if inWebDriverFullPath:        # Run with specified web driver path        lWebDriverInstance = webdriver.Chrome(executable_path = inWebDriverFullPath, options=lWebDriverChromeOptionsInstance)    else:        lWebDriverInstance = webdriver.Chrome(options = lWebDriverChromeOptionsInstance)    # Return the result    return lWebDriverInstancefrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as EC# def to extract list of offers from one jobdef OffersByJobExtractDict(inLogger, inWebDriver, inJob):    # BUG 0 - if timeout - retry the job +    # BUG 1 - do mouse scroll to to emulate user activity - cian can hold the robot    # BUG 2 - check the page to retry job offer if page is not next +    # BUG 3 - RE fall on Апартаменты-студия, 85,6 м, 4/8 этаж +    lRetryJobBool = True # Init flag if some error is raised - retry    while lRetryJobBool:        lRetryJobBool = False  # Set false until some another action will appear        lResult = copy.deepcopy(inJob) # do copy the structure        lFilterURLStr = lResult["SearchURLStr"]        inWebDriver.get(lFilterURLStr) # Open the URL        lDatetimeNowStr = str(datetime.datetime.now())        lResult.update({            "SearchDatetimeStr": lDatetimeNowStr, # Date of data extraction,  [str]            "SearchItems": {} # prepare the result        })        # Get List of the page        lNextPageItemCSS = 'div[data-name="Pagination"] li[class*="active"] + li a'        lNextPageItem = inWebDriver.find_element_by_css_selector(lNextPageItemCSS)        lPageCounterInt = 1 # Init the page counter        while lNextPageItem:            lPageNumberInt = int(inWebDriver.find_element_by_css_selector(css_selector='li[class*="--active--"] span').text) # Get the current page int from web and check with iterator (if not equal - retry all job)            if lPageNumberInt == lPageCounterInt:                lOfferListCSSStr = 'div[data-name="Offers"] > div:not([data-name="BannerServicePlaceInternal"]):not([data-name="getBannerMarkup"]):not([data-name="AdFoxBannerTracker"])'                lOfferList = inWebDriver.find_elements_by_css_selector(css_selector=lOfferListCSSStr)                for lOfferItem in lOfferList: # Processing the item, extract info                    lOfferItemInfo = { # Item URL with https                        "TitleStr": "3-комн. кв., 31,4 м, 5/8 этаж", # Offer title [str]                        "PriceFloat": 10000000.0, # Price [float]                        "PriceSqmFloat": 133333.0, # CALCULATED Price per square meters [float]                        "SqMFloat": 31.4, # Square meters in flat [float]                        "FloorCurrentInt": 5, # Current floor [int]                        "FloorTotalInt": 8, # Current floor [int]                        "RoomCountInt": 3  # Room couint [int]                    }                    lTitleStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="TopTitle"],div[data-name="Title"]').text # Extract title text                    if inLogger: inLogger.info(f"Старт обработки предложения: {lTitleStr}")                    lPriceStr = lOfferItem.find_element_by_css_selector(css_selector='div[data-name="Price"]  > div[class*="header"],div[data-name="TopPrice"] > div[class*="header"]').text # Extract total price                    lURLStr = lOfferItem.find_element_by_css_selector(css_selector='a[class*="--header--"]').get_attribute("href") # Extract offer URL                    lOfferItemInfo["TitleStr"] = lTitleStr # set the title                    lPriceStr = lPriceStr.replace(" ","").replace("","") # Remove some extra symbols                    lOfferItemInfo["PriceFloat"] = round(float(lPriceStr),2) # Convert price to the float type                    #Check if Апартаменты                    if "АПАРТАМЕНТ" in lTitleStr.upper():                        lREResult = re.search(r".*, (\d*,?\d*) м, (\d*)/(\d*) эта.", lTitleStr)  # run the re                        lOfferItemInfo["RoomCountInt"] = 1 # Room count                        lSqmStr = lREResult.group(1)                        lSqmStr= lSqmStr.replace(",",".")                        lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm count                        lOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(2)) # Floor current                        lOfferItemInfo["FloorTotalInt"] = int(lREResult.group(3)) # Floor total                        lOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M                    else:                        lREResult = re.search(r".*(\d)-комн. .*, (\d*,?\d*) м, (\d*)/(\d*) эта.", lTitleStr) # run the re                        lOfferItemInfo["RoomCountInt"] = int(lREResult.group(1)) # Room count                        lSqmStr = lREResult.group(2)                        lSqmStr= lSqmStr.replace(",",".")                        lOfferItemInfo["SqMFloat"] = round(float(lSqmStr),2) # sqm count                        lOfferItemInfo["FloorCurrentInt"] = int(lREResult.group(3)) # Floor current                        lOfferItemInfo["FloorTotalInt"] = int(lREResult.group(4)) # Floor total                        lOfferItemInfo["PriceSqmFloat"] = round(lOfferItemInfo["PriceFloat"] / lOfferItemInfo["SqMFloat"],2) # Sqm per M                    lResult['SearchItems'][lURLStr] = lOfferItemInfo # Set item in result dict                # Click next page item                lNextPageItem = None                lNextPageList = inWebDriver.find_elements_by_css_selector(lNextPageItemCSS)                if len(lNextPageList)>0:                    lNextPageItem = lNextPageList[0]                    try:                        #lNextPageItem = WebDriverWait(lWebDriver, 10).until(EC.visibility_of_element_located((By.CSS_SELECTOR, 'div[data-name="Pagination"]')))                        #lNextPageItem.click()                        inWebDriver.execute_script("""document.querySelector('div[data-name="Pagination"] li[class*="active"] + li a').click()""")                    except Exception as e:                        print(e)                    time.sleep(0.5) # some init operations                    # wait while preloader is active. If timeout - retry all job                    lTimeFromFLoat = time.time() # get current time in float (seconds)                    lDoWaitBool = True                    while lDoWaitBool:                        lPreloaderCSS = inWebDriver.find_elements_by_css_selector(css_selector='div[class*="--preloadOverlay--"]')                        if len(lPreloaderCSS)>0: time.sleep(0.5) # preloader is here - wait                        else: lDoWaitBool = False # Stop wait if preloader is dissappear                        if (time.time() - lTimeFromFLoat) > 15: # check if timeout is more than 15 seconds                            lRetryJobBool = True # Loading error on page - do break, then retry the job                            if inLogger: inLogger.warning(f"Ожидание загрузки страницы более {15} с., Робот повторит задание сначала")                            break # break the loop                    if lRetryJobBool == True: # break the loop if RetryJobBool is true                        break                lPageCounterInt = lPageCounterInt + 1 # Increment the page counter            else:                lRetryJobBool = True                if inLogger: inLogger.warning(                    f"Следующая страница по списку не была загружена. Была загружена страница: {lPageNumberInt}, Ожидалась страница: {lPageCounterInt}")        if lRetryJobBool == False:  # break the loop if RetryJobBool is true            # Check dir - create if not exists            if not os.path.exists(os.path.join('Datasets',lResult['SearchKeyStr'])):                os.makedirs(os.path.join('Datasets',lResult['SearchKeyStr']))            # Save result in file            lFile = open(f"{os.path.join('Datasets',lResult['SearchKeyStr'],lDatetimeNowStr.replace(' ','_').replace('-','_').replace(':','_').replace('.','_'))}.json","w",encoding="utf-8")            lFile.write(json.dumps(lResult))            lFile.close()# Инициализировать Google Chrome with selenium web driverlWebDriver = WebDriverInit(inWebDriverFullPath = gWebDriverFullPath, inChromeExeFullPath = gChromeExeFullPath, inExtensionFullPathList = gExtensionFullPathList)lFilterURLStr = "https://www.cian.ru/cat.php?deal_type=sale&engine_version=2&in_polygon%5B1%5D=37.6166_55.7678%2C37.6147_55.7688%2C37.6114_55.7694%2C37.6085_55.7698%2C37.6057_55.77%2C37.6018_55.77%2C37.5987_55.77%2C37.5961_55.7688%2C37.5942_55.7677%2C37.5928_55.7663%2C37.5915_55.7647%2C37.5908_55.7631%2C37.5907_55.7616%2C37.5909_55.7595%2C37.5922_55.7577%2C37.5944_55.7563%2C37.5968_55.7555%2C37.6003_55.7547%2C37.603_55.7543%2C37.6055_55.7542%2C37.6087_55.7541%2C37.6113_55.7548%2C37.6135_55.756%2C37.6151_55.7574%2C37.6163_55.7589%2C37.6179_55.7606%2C37.6187_55.7621%2C37.619_55.7637%2C37.6194_55.7651%2C37.6193_55.7667%2C37.6178_55.7679%2C37.6153_55.7683%2C37.6166_55.7678&offer_type=flat&polygon_name%5B1%5D=%D0%9E%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0&room1=1&room2=1"lJobItem = {    "SearchKeyStr": "МСК_Тверской",    "SearchTitleStr": "Москва, район Тверской",  # Title of the search [str]    "SearchURLStr": lFilterURLStr,    # URL of the CIAN search [str]}OffersByJobExtractDict(inLogger = logging, inWebDriver = lWebDriver, inJob = lJobItem)

Подведем итоги


Уважаемые роботизаторы.


Мы успешно преодолели вторую серию туториалов по созданию роботов в WEB приложениях с помощью open source pyOpenRPA. Готовый проект робота Вы можете найти в репозитории pyOpenRPA по ссылочке.
В нашем аресенале уже имеются изученные технологии упраления Desktop и WEB приложениями. В следующей статье-туториале мы остановимся на особенностях роботизированного управления мышью и клавиатурой.


Пишите комменты, внедряйте бесплатных роботов, будьте счастливы :)


До скорых публикаций!

Подробнее..

Перевод Регрессионная спираль смерти

27.07.2020 20:17:32 | Автор: admin

Перевод статьи подготовлен в преддверии старта курса Автоматизация тестирования на JavaScript




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


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


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


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


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


Конец истории


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


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


Регрессионное тестирование


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


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


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


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


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


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


Agile Delivery и бремя регрессии


Agile Delivery вводит две концепции, относящиеся к регрессионной спирали смерти. Во-первых, Agile Delivery делит фичи на небольшие, отдельные пользовательские истории (user stories). Каждая пользовательская история должна быть атомарной, чтобы ее можно было разрабатывать и тестировать как отдельный объект. Во-вторых, Agile Delivery способствует коротким циклам разработки с небольшими итеративными релизами. Все виды и гибриды Agile, Scrum, Kanban, SAFE, LeSS, ScrumBan и т. д. по-своему используют эти концепции. Давайте посмотрим на каждую в отдельности.


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


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



История 13 была внедрена и протестирована. Нам не о чем беспокоиться?
/ НЕТ! Изменения в коде могли повлиять на другие истории (как связанные, так и не связанные с этой историей)


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


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


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


Давайте рассмотрим простой гипотетический набор спринтов, чтобы прояснить, насколько быстро стоимость регрессии может отягощать скорость команды.


Предположим, у нас есть недавно созданная Agile-команда, которая только начинает свою первую итерацию разработки (спринт). На первой итерации (или некоторой начальной продолжительности в потоковой модели) у нас может быть три истории для тестирования. Тестирование этих историй довольно простое мы просто тестируем эти три истории. Во второй итерации мы добавляем две дополнительные истории. Сколько усилий будет затрачено на тестирование этих историй? Наивный ответ несколько меньше усилий, чем для первого спринта, так как мы тестируем только две истории. Однако, основываясь на нашем понимании того, что изменения кода могут вызвать регрессию в любой области приложения, мы знаем, что мы должны протестировать обе новые истории, а также регрессировать три ранее завершенные истории.



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


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


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



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


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



Автоматизация в Agile Delivery


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


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


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


Если учесть моменты двух предыдущих разделов то, что изменения в программном обеспечении могут иметь непреднамеренные последствия, и что agile delivery это просто непрерывный поток небольших изменений становится очевидным, что agile delivery не может функционировать без автоматизации тестирования. Попытка итеративной доставки без него это все равно, что отправиться в дальнюю дорогу, но отказаться от остановок, потому что остановки замедляют вас. Топливо фундаментальная часть автомобильного путешествия (на данный момент), так же, как автоматизация тестирования фундаментальная часть Agile Delivery. Автоматизация это единственное, что может смягчить удушающее бремя регрессии и избежать резкого падения скорости разработки.



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


Оставляя любое количество регрессии на трудоемкое, ручное тестирование, вы встаете на скользкую дорожку, которая может быстро привести к регрессионной спирали смерти. Если вы тестируете и автоматизируете историю, что произойдет, если из-за нехватки времени мы согласимся с правообладателем и признаем историю законченной с неполной автоматизацией? Некоторое количество бремени регрессии передается на следующую итерацию, которая затем потребляет даже больше времени на тестирование в последующем спринте. Это дает еще меньше времени для автоматизации в этой итерации, в результате чего автоматизируется еще меньше тестов, еще больше спихивая ее в будущее и т. д. Этот шаблон повторяется из итерации в итерацию, создавая цикл положительной обратной связи, пока у QA совсем не останется времени на автоматизацию, из-за долгих часов и вечеров, едва удовлетворяющих потребность в ручном тестировании, результирующих в уходе с работы и поиске компании с более зрелой agile практикой.


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


Спираль смерти


Вернемся к нашей маленькой истории.


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


Вы собираетесь протестовать, когда ваш скрам-мастер вмешивается: Мы не можем этого сделать! Это первый шаг к регрессионной спирали смерти!


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


Вы хотите, чтобы мы преуспели или просто казались преуспевающими? Эти слова вашего бизнес-аналитика, вызывает улыбку на вашем лице.


Вы решаете отменить собеседование на следующей неделе.


Слова благодарности


Я не изобрел термин регрессионная спираль смерти, но никогда не видел его в письменной форме. Самое раннее использование термина, которое я могу найти, находится в Тестировании экстремального программирования (2002) Лизы Криспин и Тип Хаус.


Несколько других ссылок можно найти с помощью быстрого поиска в Google:


http://ivory.idyll.org/blog/software-quality-death-spiral.html (2008)


https://xebia.com/blog/regression-testing-with-an-agile-mindset (2010)




Узнать всё о курсе Автоматизация тестирования на JavaScript



Подробнее..

GitOps очередной модный термин или прорыв в автоматизации?

06.10.2020 12:23:44 | Автор: admin

Большинство из нас, подмечая очередной новый термин в IT блогосфере или конференции, рано или поздно задается подобным вопросом: Что это? Очередное модное слово, buzzword или действительно что-то стоящее пристального внимания, изучения и обещающее новые горизонты? Точно также вышло у меня и с термином GitOps некоторое время назад. Вооружившись множеством уже существующих статей, а также знанием коллег из компании GitLab, я попытался разобраться, что же это за зверь, и как его применение может выглядеть на практике.

Кстати, о новизне термина GitOps также говорит недавно проведенный нами опрос: более половины опрошенных еще не начинали работы с его принципами.

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

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

Так в чем собственно отличие GitOps от IaC? Именно с этого вопроса я и начал свое расследование. Пообщавшись с коллегами, у меня получилось выработать следующее сравнение:

GitOps

IaC

Весь код хранится в git репозитории

Версионность кода необязательна

Декларативное описание кода / Идемпотентность

Допустимо как декларативное, так и императивное описание

Изменения вступают в силу с использованием механизмов Merge Request / Pull Request

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

Процесс выката обновлений автоматизирован

Процесс выката обновлений не нормирован (автоматический, ручной, копирование файлов, с использованием командной строки и т.д.)

Иными словами GitOps появился на свет именно благодаря применению принципов IaC. Во-первых, инфраструктуру и конфигурации теперь можно было хранить точно так же как и приложения. Код легко хранить, им легко делиться, сравнивать, пользоваться возможностями версионности. Версии, ветки, история. И все это в общедоступном для всей команды месте. Поэтому вполне закономерным развитием стало использования систем контроля версий. В частности, git, как наиболее популярного.

С другой стороны, появилась возможность автоматизировать процессы управления инфраструктурой. Теперь это можно сделать быстрее, надежнее и дешевле. Тем более принципы CI / CD уже были известны и популярны среди разработчиков программного обеспечения. Необходимо было только перенести и применить уже известные знания и навыки в новую область. Эти практики однако выходили за рамки стандартного определения Инфраструктуры как кода, отсюда и родилось понятие GitOps.

Любопытность GitOps, конечно, еще и в том, что это не продукт, плагин или платформа, связанная с каким бы то ни было вендором. Это скорее парадигма и набор принципов, аналогично с другим знакомым нам термином: DevOps.

В компании GitLab мы выработали два определения этого нового термина: теоретический и практически. Начнем с теоретического:

GitOps - это методология, которая использует передовые принципы DevOps, используемые для разработки приложений, такие как контроль версий, взаимодействие, согласование, CI/CD, и применяет их для решения задач по автоматизации управления инфраструктурой.

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

С практической же точки зрения мы описываем GitOps следующим образом:

GitOps = IaC + MRs + CI/CD

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

Merge Request (альтернативное название Pull Request). В плане процесса MR - это запрос на применение изменений кода и последующее слияние веток. Но в плане инструментов, которыми мы пользуемся - это скорее возможность для получения полной картины всех вносимых изменений: не только code diff, собранный из какого-то количества коммитов, но и контекст, результаты тестов, конечный ожидаемый результат. Если мы говорим про инфраструктурный код, то нам интересно, как именно изменится инфраструктура, сколько новых ресурсов будет добавлено или удалено, изменено. Желательно в каком-то более удобном и легко читаемом формате. В случае с облачными провайдерами неплохо было бы знать, какие финансовые последствия понесет это изменение.

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

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

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

А если вам вдруг станет интересно, как это все выглядит на практике, то приглашаю посмотреть наш мастер-класс, в котором я пошагово рассказываю, как при помощи GitLab:

  • Реализовать основные принципы GitOps

  • Создавать и вносить изменения в облачную инфраструктуру (на примере Yandex Cloud)

  • Автоматизировать обнаружение дрейфа системы от желаемого состояния при помощи активного мониторинга

http://personeltest.ru/aways/bit.ly/34tRpwZhttps://bit.ly/34tRpwZ
Подробнее..

Настраиваем и автоматизируем развёртывание Active Directory

03.11.2020 08:13:00 | Автор: admin


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


  1. Вместо простого "Далее, Далее, кликаем сюда, вбиваем это" я постарался дать внятное объяснение каждому шагу, каждой настройке, которую предстоит выполнить. Помимо основных понятий Active Directory, DNS и DHCP вы также сможете найти много интересной информации по всем галочкам, которые вы часто видели, но не задумывались об их назначении.
  2. В конце статьи я предложу способ автоматизировать развёртывание получившегося стенда полностью с нуля, имея на компьютере только iso-образы ОС Windows 7 и Windows Server 2016. И никакого PowerShell. Всего одной командой.

Статья предполагает наличие у читателя лишь самых начальных знаний об устройстве сетей (на уровне "Что такое IP-адрес и DNS-адрес").


Заинтересовало что-то из вышеперечисленного? Тогда погнали.


Туториал будет происходить не в вакууме, а на конкретном виртуальном стенде, состоящим из двух виртуальных машин:



Начальное состояние стенда:


  1. На машине windows_server уже установлена ОС Windows Server 2016 Standard Evaluation (с GUI). Машина находится в состоянии "сразу после установки ОС". В процессе туториала на ней будут развернуты службы Active Directory (с доменом mydomain.com), DNS и DHCP.


  2. Машина workstation выполняет роль рабочей станции. На ней установлена ОС Windows 7. Машина находится в состоянии "сразу после установки ОС". В процессе туториала она будет подключена к домену mydomain.com.



Туториал построен следующим образом (если вам интересен только конкретный пункт смело кликайте прямо туда):


  1. Объясню, почему я выбрал именно такой стенд для туториала;
  2. Супер-краткое описание технологии Active Directory;
  3. Выполняется небольшая предварительная настройка windows_server;
  4. На windows_server производится включение необходимых компонентов;
  5. На windows_server происходит настройка контроллера домена AD (совместно с DNS);
  6. На windows_server происходит настройка сервера DHCP;
  7. На windows_server регистрируется новая учетная запись в AD;
  8. На workstation происходит подключение к домену.

В конце туториала вас ждет приятный бонус я покажу вам как можно развернуть у себя на компьютере весь этот работающий стенд одной единственной командой. Вам понадобится только наличие двух установочных iso-образов (windows 7 и windows server 2016), да небольшой скрипт, ссылку на который я вам дам в конце статьи.


Почему такой стенд?


Такой стенд, с моей точки зрения, отлично подходит для первого самостоятельного "прощупывания" технологии Active Directory. Он минималистичен (всего 2 виртуальные машины), занимает минимум ресурсов, но при этом изолирован и самодостаточен. Его можно развернуть даже на довольно средненьком компьютере и ноутбуке. При этом на стенде уже присутствуют основные сетевые службы (AD + DNS). DHCP хоть и необязателен для функционирования AD, всё равно был добавлен в стенд в ознакомительных целях.


Disclaimer

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


Туториал предполагает подробный разбор всех шагов по настройке, с пояснениями "что, зачем и почему". Туториал ориентирован на людей, не слишком знакомых с технологиями Active Directory, DNS и DHCP, которые хотели бы немного узнать о внутренней кухне администрирования сетей с Active Directory.


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


Что такое Active Directory


Active Directory это службы каталогов от компании Microsoft, как подсказывает нам Википедия. За этим сухим и невзрачным определением скрывается одна из важнейших технологий в администрировании сетей. Благодаря Active Directory администоратор сети получает очень удобное централизированное средство управления учетными записями пользователей, групповыми политиками (в т.ч. политиками безопасности) и объектами в сети (причём Active Directory без особых проблем справляется даже с гигантскими сетями). А благодаря встроенному механизму репликации, "положить" правильно настроенные сервисы AD не так-то просто. Ну и напоследок, благодаря Windows, настроить Active Directory можно буквально мышкой, так что даже совсем начинающие IT-шники смогут с этим справиться.


Несмотря на то, что технологией заведует Microsoft, она вовсе не ограничивается управлением Windows-машин все известные Linux-дистрибутивы уже давным давно научились работать с этой технологией. Повстречаться с Active Directory не просто, а очень просто практически каждый офис предполагает наличие этой технологии, поэтому даже самым заядлым линуксоидам было бы неплохо разбираться в азах работы Active Directory.


Начинаем


Вы установили Windows Server 2016 и (надеюсь) видите следующий экран:



Эта панель основное (графическое) средство администрирования Windows Server 2016. Здесь вы можете управлять компонентами и сервисами на вашем сервере (проще говоря, настраивать то, что умеет делать сервер). Эту же панель можно использовать и для базовых сетевых настроек Windows Server, для чего есть вкладка "Локальный сервер".


Базовые настройки Windows Server


Первое, что нужно сделать это поменять сетевое имя сервера.


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


Проблема в том, что по-умолчанию для Windows Server генерируется совершенно нечитаемое и неинформативное сетевое имя (я выделил его красным цветом на скриншоте).


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


Смена сетевого имени

Нужно кликнуть на текущее имя сервера (отмечено красным цветом), затем во вкладке "Имя компьютера" нажать на кнопку "Изменить...", после чего ввести что-то более благоразумное:



После смены имени машину нужно будет перезагрузить.


Теперь зададим статический IP-адрес для сервера. В принципе это делать не обязательно, раз мы всё равно собрались поднимать DHCP службу, но на самом деле это хорошая практика, когда все ключевые элементы корпоративной сети имеют фиксированные адреса. Открыть меню по настройке сетевого адаптера можно из вкладки "Локальный сервер", кликнув на текущие настройки Ethernet-адаптера (тоже выделены красным цветом).


Настройки IP для интерфейса windows_server


Включаем нужные компоненты


Для нашего стенда нам понадобится включить следующие сервисы (или, как они тут называются, роли) на Windows Server:


  • Доменные службы Active Directory;
  • DNS-сервер;
  • DHCP-сервер.

Пройдемся вкратце по каждому из них.


Доменные службы Active Directory


Эта роль фактически "включает" технологию Active Directory на сервере и делает его контроллером домена (под доменом в технологии AD понимается группа логически связанных объектов в сети). Благодаря этой роли администратор получает возможность управлять объектами в сети, а также хранить информацию о них в специальной распределенной базе данных.


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


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


Однако этот туториал рассчитан на простое ознакомление с технологией AD "на виртуалках", поэтому здесь не будет рассматриваться вопрос создания нескольких контроллеров AD в одном домене.


С этим пунктом все более менее понятно, а зачем же нам включать дополнительно ещё DNS-сервер?


DNS-сервер


Обычно протокол DNS (Domain Name System) используется для обращения к узлам в сети не по их IP-адресу, а по доменному имени (строковый идентификатор), что, конечно, гораздо удобнее. Другими словами, DNS чаще всего используется для разрешения доменных имен.


Но область применения протокола DNS не ограничивается только сопоставлением хостового имени и IP-адреса, что как раз подтверждает технология Active Directory. Дело в том, что Microsoft решила построить технологию Active Directory не с нуля, а на основе протокола DNS. В частности, протокол DNS используется при определении местонахождения всех ключевых сервисов Active Directory в сети. Другими словами, рабочая станция при подключении к контроллеру домена понимает, "куда" ей надо обращаться, именно с помощью протокола DNS.


Все DNS-записи (в том числе с информацией о сервисах Active Directory) хранятся на DNS-сервере, а это значит, что нам нужно заиметь свой собственный DNS-сервер! Вот только вопрос, откуда его взять? Есть два варианта:


  1. Использовать отдельную машину в роли DNS-сервера;
  2. Использовать саму машину windows_server в роли DNS-сервера.

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


Именно поэтому эту роль (DNS-сервера) тоже нужно добавить к ролям машины windows_server.


Кстати, если не добавить роль "DNS-сервер" сейчас, то в будущем у вас ещё будет такая возможность при конфигурировании контроллера домена AD.


DHCP-сервер


Протокол DHCP (Dynamic Host Configuration Protocol) нужен для автоматической выдачи сетевых настроек узлам в сети. Под сетевыми настройками понимается IP-адрес, адрес шлюза по-умолчанию, адрес DNS-сервера, и ещё ряд других настроек. Этот протокол чрезвычайно удобен при администрировании сетей, особенно больших.


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


Протокол DHCP не имеет никакого отношения к технологии Active Directory, и можно было бы обойтись вовсе без него (достаточно прописать все сетевые настройки на рабочей станции самостоятельно), но я решил включить этот протокол в данный туториал просто для общего ознакомления. К тому же, такая связка "Контроллер AD DNS-сервер DHCP-сервер" довольно часто встречается в реальной жизни, потому что это очень удобный набор сервисов.


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


Что ж, довольно теории, давайте лучше перейдём к включению этих самых ролей.


Мастер добавления ролей и компонентов


Возвращаемся на панель мониторинга (самый первый скриншот) и щелкаем на пункт "Добавить роли и компоненты". Вас поприветствует мастер добавления ролей и компонентов. Первый экран ("Перед началом работы") пропускаем, он совсем неинтересный, а вот дальше идёт экран "Выбор типа установки"


Выбор типа установки


Нас устраивает значение по-умолчанию (Установка ролей или компонентов"), но интересен и второй пункт он позволяет задействовать ещё одну возможность Windows Server инфраструктуру виртуальных рабочих мест (Virtual Desktop Environment VDI). Эта интереснейшая технология позволяет, буквально, виртуализировать рабочее место. То есть для пользователя создаётся виртуальное рабочее место, к которому он может подключаться через тонкий клиент. Пользователь лишь видит картинку, тогда как само рабочее место может совершенно прозрачно работать где угодно.


Впрочем, технология VDI это отдельная большая тема, а в этом туториале надо сосредоточиться на контроллере AD, так что кликаем "Далее" и видим экран выбора целевого сервера.


Выбор целевого сервера


Мастер добавления ролей позволяет устанавливать роль не только на текущую машину, но вообще на любой добавленный сервер, и даже на виртуальный жёсткий диск. Да, если ваша Windows Server развернута на виртуальной машине (а это довольно частое явление), то вы можете администрировать эту виртуальную машину даже не запуская её! Понаблюдать за этим процессом можно, например, здесь.


Нам же такая экзотика ни к чему, так что просто выбираем единственный возможный сервер (обратите внимание, что он теперь называется ADController место непонятной абракадабры), жмём "Далее" и, наконец, попадаем на экран выбора ролей, которые нужно добавить.


Выбор добавляемых ролей


Выбираем три роли, о которых уже говорили ранее, и продолжаем.


Выбор компонентов


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


Согласно идеологии Microsoft, роль это набор программ, которые позволяют компьютеру предоставлять некоторые функции для пользователей в сети. Например, DNS, DHCP, контроллер домена AD это всё роли. А вот компоненты это набор программ, которые улучшают либо возможности ролей сервера, либо самого сервера.


При этом глядя на список "Компонентов" так сходу и не скажешь, что какие-то вещи в списке лишь "вспомогательные". Вот например, DHCP-сервер расценивается как роль, а WINS-сервер уже как компонент. А чем SMTP-сервер хуже DNS?


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


В любом случае, дополнительные компоненты нам не нужны, так что кликаем "Далее".


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


Подтверждение устанавливаемых ролей и компонентов


На экране подтверждения ещё раз видим все устанавливаемые роли и компоненты, после чего жмём "Установить".


Остаётся лишь дождаться, когда заполнится progress-bar, и перейти к следующему пункту туториала настройке контроллера домена AD.


Настраиваем контроллер домена Active Directory


Все роли и компоненты успешно добавлены, о чём свидетельствует следующий экран:



Вот только AD на сервере всё еще не работает для этого его необходимо донастроить. Для этого нам настойчиво предлагают "Повысить роль этого сервера до уровня контроллера домена".


Погодите-ка, ЧТО?!


А чем же я занимался последние 15 минут? Я же добавлял роли, и судя по сообщению, они успешно добавились! И тут меня снова хотят заставить добавлять какие-то новые роли? В чем-то тут подвох.


Подвох тут действительно имеется, но вообще в не самом очевидном месте. Вот так выглядит предыдущий скриншот в английской версии Windows Server (картинка из интернета).


Английская версия скриншота


Видите разницу? В английской версии ни слова ни про какие роли! Про повышение есть, про роли нет. Один из тех случаев, когда перевод вносит сумятицу на пустом месте. Согласно английской версии, никакими ролями мы дальше не занимаемся, что и логично, ведь мы их как раз только что добавили.


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


Конфигурация развёртывания


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



Технология Active Directory (как и DNS) подразумевает иерархическое построение имён на основе доменов. Домены могут выстраиваться в доменные деревья по принципу "родительско-дочерних" отношений. В основе дерева лежит так называемый корневой домен (на картинке выше это sources.com, xyz.com и abc.com). При этом домен может иметь сколько угодно потомков. Домен-потомок располагается в просранстве имён родителя и является его "поддоменом" (subdomain). У доменного имени домена-потомка есть дополнительный префикс относительно доменного имени родителя (rus.abc.com, eng.abc.com). Один корневой домен основывает только одно доменное дерево со своим независимым пространством имён.


Теперь представьте, что таких независимых деревьев может быть много в этом случае эти деревья образуют структуру, которая называется "лес". При этом в Active Directory доменные деревья не могут быть "сами по себе" они обязательно должны находиться в лесу (даже если лес будет состоять всего из одного-единственного домена). Первый домен, который добавляется в лес, называется корневым доменом леса (на рисунке выше это sources.com). Корневой домен леса используется для идентификации всего леса (то есть если корневой домен называется sources.com, то и весь лес называется sources.com).


Теперь возвращаемся к мастеру настройки доменных имен. На этом этапе мастер предлагает следующие варианты:


  1. Добавить контроллер домена в существующий домен (помните про резервирование контроллеров в домене, так ведь?). Этот вариант не для нас, ведь домена ещё никакого нет;
  2. Добавить новый домен в лес. Этого мы тоже сделать не можем, т.к. и леса у нас тоже никакого нет;
  3. Добавить новый лес. Это вариант как раз для нас. При этом нам тут же предлагают выбрать корневой домен для этого леса (первый домен, который будет создан в лесу).

Назовём корневой домен mydomain.com и кликнем "Далее"


Параметры контроллера домена


Рассмотрим возможные параметры:


  1. Режим работы леса и домена. Домены в одном лесе могут работать в разных режимах в зависимости от версии Windows Server на борту. Лес должен иметь режим не выше, чем самый "старый" домен в его составе. Т.к. мы планируем использовать только Windows Server 2016, то оставим этот режим и для леса и для домена;
  2. DNS-сервер. Если ранее Вы не активировали роль DNS-сервера в мастере добавления ролей, то можете сделать это сейчас (вам даже предложат такой вариант по-умолчанию);
  3. Должен ли контроллер домена выступать в роли Global Catalog-сервера;
  4. Включить ли режим базы данных Active Directory "только на чтение". Основная задача, которую преследует технология RODC возможность безопасной установки собственного контролера домена в удаленных филиалах и офисах, в которых сложно обеспечить физическую защиту сервера с ролью DC. Контроллер домена RODC содержит копию базы Active Directory, доступную только на чтение. Это означает, что никто, даже при получении физического доступа к такому контроллеру домена, не сможет изменить данные в AD (в том числе сбросить пароль администратора домена) (информация взята отсюда)

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


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


Но что же делать, если рабочей станции нужно получить информацию об объекте из другого домена? И вот тут в дело вступает ещё один важнейший механизм технологии Active Directory, который разывается глобальный каталог.


Что такое вообще "Глобальный каталог"? Согласно Miscrosoft это распределенное хранилище данных, которое хранит частичное представление обо всех AD-объектах в лесу. Это хранилище располагается на котроллерах домена, которые имеют дополнительную роль "Global Catalog Server" (Сервер глобального каталога). От обычного кнтроллера домена GC-сервер отличается в первую очередь тем, что помимо полной копии всех объектов в своем домене, хранит также частичную информацию обо всех объектах в других доменах леса.


Чего это позволяет достичь? Давайте представим, что рабочая станция запросила информацию об объекте из другого домена. Она обращается на GC-сервер с просьбой предоставить ей информацию об этом объекте. GC-сервер, в свою очередь, может:


  1. Либо отдать рабочей станции нужную информацию сразу (если эта информация у GC-сервера имеется);
  2. Либо перенаправить запрос к нужному контроллеру домена, где эта информация точно будет находиться. Чтобы понять, какому контроллеру домена нужно перенаправить запрос, как раз и происходит поиск по GC.

Информация о том, какие атрибуты попадают в глобальный каталог, определена в Partial Attribute Set (PAS), который может настраивать администратор AD. Например, если администроатр понимает, что рабочие станции часто будут обращаться к атрибуту, который не содержится в глобальном каталоге, он может добавить туда этот атрибут. Тогда запросы рабочих станций при чтении этого атрибута будут выполняться значительно быстрее, т.к. уже ближайший GC-сервер сможет предоставить им всю нужную информацию.


Однако, если в лесе всего один домен (как у нас), то Глобальный каталог содержит полную копию объектов в домене и всё.


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


Что ж, давайте согласимся с этим "выбором" мастера и перейдём к последнему параметру на этом скриншоте к паролю для режима восстановления служб каталогов. Это особый режим безопасной загрузки Windows Server, который позволяет администратору работать с базой данных AD. Этот режим применяется, например, в следующих случаях:


  • база Active Directory повреждена и нуждается в исправлении;
  • требуется выполнить обслуживание базы данных AD (сжатие, анализ на наличие ошибок);
  • требуется восстановить резервную копию базы данных AD;
  • требуется сменить пароль администратора.

Да да, вы не ослышались. Чтобы просто восстановить резервную копию базы данных, нужно перезагрузить машину и загрузиться в особом "безопасном" режиме. Это вам не Linux какой-нибудь.


Фух, вроде разобрались. Давайте перейдем дальше на шаг, где нам предложат настроить делегирование DNS.


Делегирование DNS


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


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


NetBIOS-имя


Мы видим, что мастер предложил нам на выбор сразу же имя для нашего домена MYDOMAIN. Но вы можете (и должны) задать себе вопрос: а что такое вообще NetBIOS-имя и зачем оно нужно? И разве мы уже не настраивали сетевое имя узла (Hostname) в самом начале? Чего же от вас хотят?


NetBIOS (Network Basic Input/Oputout) это ещё один способ разрешения имён узлов в сети (более древний и более примитивный, чем DNS). NetBIOS-имена не предполагают никакой иерархии, их длина ограничивается всего лишь 16 символами, и они применяются только для разрешения имён компьютеров в локальной сети. Когда мы в самом начале туториала выбрали сетевое имя ADController мы, на самом деле, задали именно NetBIOS-имя для сервера. Но теперь от нас снова требуют выбрать NetBIOS-имя (да ещё и другое, отличное от ADContoller). Не много ли NetBIOS-имён для одного компьютера?


Дело в том, что Microsoft пошла ещё дальше и ограничила длину NetBIOS-имен не 16 символами, а 15 символами. 16-ый символ при этом считается зарезервированным суффиксом, который может принимать фиксированные значения. В зависимости от значения 16-го байта получаются разные классы NetBIOS-имён. Например, если суффикс равен 00, то NetBIOS-имя относится к рабочей станции. Если суффикс равен 1С, то это имя относится к имени домена.


То есть, как вы понимаете, на первом шаге мы задавали NetBIOS-имя для компьютера Windows Server (с суффиком 00). А теперь задаём NetBIOS-имя домена mydomain.com (с суффиксом 1С).


Кстати, можете, ради интереса, отмотать туториал в самое начало и посчитать количество символов в "нечитаемом" автоматически сгенерированном сетевом имени windows_server. Будет как раз 15 символов (максимальная длина NetBIOS-имени).


И напоследок скажу, что вы не можете пропустить этот шаг. NetBIOS хоть и устаревшая технология, но до сих пор используется ради совместимости с некоторыми старыми службами. Настроить контроллер домена Active Directory без NetBIOS-имени нельзя.


Что ж, и с этим тоже разобрались. Оставляем NetBIOS-имя по-умолчанию и двигаемся дальше, к выбору места расположения базы данных AD. Можно оставить значение по-умолчанию, комментировать особо нечего.


Расположение базы данных


Все ваши настройки должны пройти предварительную проверку:


Проверка предварительных требований


Как только всё готово, жмите "Установить" и спокойно идёте пить чай, потому что после установки автоматически начнётся очень-очень долгая перезагрузка. Зато настройка контроллера домена AD на этом закончена, поздравляю!


Настройка DHCP-сервера


Пришло время заняться настройкой DHCP-сервера. Настройка глобально состоит из двух частей:


  1. Авторизация DHCP-сервера в домене AD. Не каждый DHCP-сервер может раздавать сетевые настройки в домене AD только авторизованные. Это сделано с целях безопасности, чтобы другие DHCP-серверы не могли "подсунуть" неправильные настройки компьютерам в домене;
  2. Настройка новой DHCP-области. Это уже непосредственно настройка самого DHCP-сервера, в ходе которой определяются какие сетевые настройки будут выдаваться компьютерам в сегменте сети.

Для того, чтобы авторизировать DHCP-сервер, нужно вернуться на панель мониторинга (она и так должна быть перед вами после перезагрузки), перейти на вкладку DHCP (слева) и кликнуть на предложение донастроить DHCP-сервер:


Запуск авторизации DHCP-сервера




В открывшемся мастере настройки DHCP после установки пропускаем первый приветственный экран и переходим к экрану авторизации


Авторизация DHCP-сервера в домене


На выбор предлагаются три варианта:


  1. Использовать учётные администратора (по-умолчанию)
  2. Использовать учётные данные другого пользователя;
  3. Пропустить авторизацию AD.

По-умолчанию авторизовать DHCP-сервер в домене могут только члены группы EnterpriseAdmins, куда как раз и входит пользователь MYDOMAIN\Администратор. При желании можно потратить немного времени и делегировать эту возможность админам "помельче" (региональным администраторам), подчерпнуть больше информации по этой теме можно отсюда.


Итак, выбираем вариант по-умолчанию и завершаем первый этап настроки DHCP-сервера.


Завершение авторизации DHCP-сервера


Теперь переходим непосредственно к настройкам DHCP. Для этого на панели мониторинга кликаем вкладку "Средства" и выбираем пункт "DHCP"



В открывшемся окне с настройками DHCP нужно кликнуть правой кнопкой мышки на IPv4 и затем на пункт меню "Создать область". После этого откроется мастер создания новой области.


Открытие мастера создания новой области



Что такое DHCP-область? Под этим понимается некий диапазон IP-адресов, которые может выдавать DHCP-сервер другим компьютерам в сети. Каждая область помимо диапазона IP-адресов также содержит другие сетевые настройки, с которыми мы сейчас и познакомимся.


Назовём DHCP-область SCOPE1 и перейдём дальше.


На следующем экране вам предложат выбрать диапазон адресов, которые будут выдаваться компьютерам в сети. Ранее я настраивал сетевой интерфейс на Windows Server, выдав ему адрес 192.168.1.1/24. Это статический адрес и он зарезервирован, его выдавать другим компьютерам нельзя.


Зато никто не мешает выдавать все остальные адреса в сети 192.168.1.0/24 так что задаём диапазон от 192.168.1.2 до 192.168.1.254 (192.168.1.255 это зарезервированный широковещательный адрес, его выдавать тоже нельзя).


Настройка диапазона адресов


В целом, никто не мешает вам как администратору выдавать меньше IP-адресов, чем доступно в адресации сети. Например, можно было бы выделить в сети всего 100 адресов для автоматической выдачи: 192.168.1.101-192.168.1.200.


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


Исключения в диапазоне и задержка DHCPOFFER


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


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


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


Настройка времени аренды адресов


Протокол DHCP предполагает выделение адресов только на определённое время, после чего компьютеры должны продлять аренду. Здесь можно настроить это время (по-умолчанию 8 дней).


8 дней меня лично вполне устраивает, так что кликаем "Далее" и видим предложение настроить другие настройки, которые будут получать клиенты в сети (помимо IP-адреса). Соглашаемся.


Настроить дополнительные параметры


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


Шлюз по-умолчанию


Далее идет настройка DNS. Здесь можно задать имя родительского домена и адреса DNS-серверов. С адресами DNS-серверов всё более-менее понятно это IP-адреса серверов, куда следует обращаться клиентам за помощью в разрешении DNS-имён. Сейчас в этом списке фигурирует тот же адрес, что мы добавили как шлюз по-умолчанию.


А вот для понимания имени родительского домена, рассмотрим следующую ситуацию.


Допустим, есть домен mydomain.com и есть два компьютера в этом домене с именами comp1.mydomain.com и comp2.mydomain.com. Если comp1 хочет связаться с comp2, то он должен, по-хорошему, использовать следующую команду (обращение по Fully Qualified Domain Name FQDN):


ping comp2.mydomain.com

Но задумывались ли вы когда-нибудь, что именно произойдет, если попытаться пропинговать другой узел следующим образом?


ping comp2

На самом деле, в этом случае начинается целая магия очень хитрый процесс разрешения имён (картинка из интернетов).


Процесс разрешения сетевых имён


  1. Поиск информации в hosts.txt или в кеше;
  2. Попытка найти имя через DNS;
  3. Попытка найти NetBIOS-имя в кеше;
  4. Попытка найти NetBIOS-имя через WINS-сервер;
  5. Попытка найти NetBIOS-имя путём отправки широковещательных пакетов в локальную сеть;
  6. Попытка найти NetBIOS-имя в LMHOSTS-файле.

Согласно алгоритму разрешения сетевых имен, сначала comp1 попробует найти информацию о comp2 в hosts.txt файле. Если этой информации там не окажется, то начинается процесс поиска узла через DNS. Вот только вопрос DNS-имена же находятся в каком-то домене, верно? Какое доменное имя нужно "пристыковать" к comp2 при выполнении пинга?


Вот тут в дело и вступает настройка DHCP, которая называется "имя родительсокго домена". Это как раз тот суффикс, который будет автоматически "пристыкован" к имени comp2 при выполнении DNS-разрешения имени. Так что если имя родительского домена равно "mydomain.com", то команда ping comp2 неявно преобразуется в ping comp2.mydomain.com.


Если же DNS-разрешение окажется неудачным, дальше начнутся попытки найти comp2 уже по NetBIOS-имени. Что такое WINS, и чем он отличается от Broadcast информация будет чуть дальше по тексту.


Что ж, в нашем случае имя родительсокго домена должно быть mydomain.com (значение по-умолчанию), а нужный DNS-сервер уже находится в списке, так что в итоге просто кликаем "Далее".


Настройки DNS


Теперь нас попросят указать настройки WINS-сервера. WINS (Windows Internet Name Service) сервер участвует в разрешении NetBIOS-имён в сети (прямо как DNS-сервер для DNS-имён). Вот только, в отличие от DNS, WINS-сервер не обязательно должен присутствовать в сети, чтобы разрешение NetBIOS-имён работало. Так зачем же он нужен тогда?


Дело в том, что по-умолчанию разрешение NetBIOS-имен происходит через широковещательные запросы. С одной стороны, это очень простой механизм (проще не придумаешь), но, с другой стороны, обладает парой недостатков:


  1. При наличии большого количества NetBIOS-имён в сети широковещательный тафик может начать "зашумлять" канал;
  2. Широковещательные запросы не могут "выйти" за пределы текущей сети, поэтому узнать NetBIOS-имя из другой сети таким способом не выйдет.

Так вот, WINS-сервер позволяет решить обе этих проблемы. Этот сервер централизованно хранит NetBIOS-имена компьютеров, и обычные узлы в сети могут обращаться к нему для поиска IP-адреса интересующего их имени (как и для DNS). Такой подход, во-первых, резко уменьшает количество широковещательного трафика в сети, а, во-вторых, позволяет посылать NetBIOS-запросы в другие сети, а не только в текущую.


В нашей небольшой сети WINS-сервер нам ни к чему, поэтому просто пропускаем эту настройку и едем дальше.


WINS-серверы


В последнем шаге настройки вам предлагают сразу активировать настроенную область. Соглашаемся, и на этом заканчиваем настройку DHCP.


Активация области


Создаём нового пользователя в домене AD


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


Для этого возвращаемся на панель мониторинга, кликаем на "Средства" и затем на "Пользователи и Компьютеры Active Directory"



В открывшемся меню можно заметить созданный домен mydomain.com и его состав. Видно, что помимо пользователей в домене созданы папочки для Computers, Domain Controllers и других сущностей. Но нас сейчас интересуют пользователи, поэтому кликаем правой кнопкой мышки на папке Users и выбираем "Создать" -> "Пользователь"



После этого появляется диалоговое окно с преложением ввести данные нового пользователя. По старой доброй традиции назовём пользователя Foo Bar. Обратите внимание, что пользователь отображается лишь как "Объект" в Active Directory наравне с другими объектами.


Новый объект - Пользователь


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


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


Параметры нового пользователя


После этого останется лишь подтвердить создание нового пользователя.


Подтверждение создания нового пользователя


Ну что ж, вот, кажется, и всё! Осталось лишь проверить ввод рабочей станции в домен.


Ввод рабочей станции в домен


Переключаемся на вторую машину workstation под управлением Windows 7 и заходим в свойства системы. Сейчас видно, что рабочая станция находится в рабочей группе (не в домене). Кстати говоря, WORKGROUP это тоже NetBIOS-имя. Только в отличии от имени домена оно имеет суффикс 1E.



Щелкаем на кнопку "Изменить параметры", затем в появившемся окне ещё раз "Изменить...".


В окне изменения имени компьютера пишем, что он должен принадлежать домену mydomain.com.


Подключение к домену


Видим предупреждение о нестандартном имени компьютера (testo-ПК содержит кириллицу). Это связано с тем, что NetBIOS-имена не могут содеражать кириллицу. Но мы с вами настроили DNS-сервер (DNS настройки прилетели на рабочую станцию по DHCP), а DNS-механизм разрешения имён, как мы знаем, имеет приоритет перед NetBOIS. Так что в данном случае на работоспособность AD кириллица не влияет. Но на практике так делать не надо!


Нестандартное имя компьютера


Вводим логин-пароль от новой учетной записи FooBar и, наконец, видим заветное сообщение "Добро пожаловать в домен"


Авторизация в домене



После ввода компьютера в домене необходимо перезагрузить компьютер, ну а дальше вводим учётные данные пользователя в AD.


Логин в AD


И после успешного входа на рабочий стол перепроверяем свойства системы.


Новые свойства системы


Полное имя компьютера поменялось на testo-ПК.mydomain.com, а это значит, что мы успешно ввели рабочую станцию в домен mydomain.com.


Автоматизируем


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


Так почему бы не автоматизировать все действия с клавиатурой и мышкой, которые мы предпринимали? И нет, я говорю не об AutoIT, я говорю о платформе Testo, создателем которой я являюсь. Эта платформа позволяет фиксировать все действия, проводимые с виртуальными машинами, в виде скриптов на специальном языке Testo-lang. Ну а Testo затем превратит эти скрипты обратно в действия.


Я приведу лишь один скриншот с кусочком скрипта, чтобы у вас сложилось представление о том, о чём я говорю (да, именно скриншот, ведь хабр не умеет подсвечивать скриповый язык Testo-lang). Я даже не буду комментировать этот скрипт, т.к. верю, что код говорит сам за себя.


Секция скрипта на языке Testo-lang


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



Всё, что Вам потребуется для создания собственного стенда с настроенной Active Directory это:


  1. Установочный iso-образ Windows Server 2016 русской версии;
  2. Установочный iso-образ Windows 7 (придётся поискать самим);
  3. Скрипты на языке Testo-lang;
  4. Установленная платформа Testo (бесплатно);
  5. Выполнить команду.

sudo testo run ./tests.testo --param ISO_DIR /path/to/your/iso/dir

И всё. Как и я обещал всего одна команда. Через пол часа час (зависит от шустрости вашего компьютера) вы сможете наслаждаться своим готовым стендом.


Итоги


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


  • сайт платформы Тесто.
  • youtube-канал, где можно найти много примеров.
  • основная статья на хабре
  • статья, где я автоматизировал несколько системных тестов для Dr. Web
  • скрипты на языке Testo-lang для этого туториала
Подробнее..

Тестирование From Zero to Hero. Часть 1

28.01.2021 14:21:54 | Автор: admin

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

Рассказ будет в трех частях:

  • Трудности, с которыми нам пришлось столкнуться, и как мы их преодолевали.

  • Конкретные решения по некоторым распространенным кейсам при написании интеграционных тестов.

  • Подход к написанию E2E-тестов (тестов, покрывающих взаимодействие всех систем приложения, включая back-end и пользовательский интерфейс) с использованием паттерна PageObject, пришедшего к нам из мира веб-разработки.

Вместо предисловия

Было время, когда солнце светило ярче, орки были зеленее, а мобильный банк Тинькофф писали восемь человек. Тогда почти все фичи катились без какого-либо тестового покрытия. В лучшем случае основные компоненты бизнес-логики могли быть покрыты Unit-тестами.

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

Этап принятия

Ранее E2E-тесты писали только автоматизаторы, которых было несколько человек. Тестами покрывалась только малая часть основного функционала. Регресс-тестирование начало затягиваться, и выкатывать фичи в прод стало труднее с учетом роста команды.

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

Тест-кейсы

Тест-кейсы это шаги, по которым QA-специалист проходит во время тестирования фичи. Сюда входят всевозможные проверки пользовательского флоу, реакция приложения на разного рода corner-case-проверки наличия UI-элементов на экране и так далее.

Пример тест кейсаПример тест кейса

Тут мы столкнулись с несколькими проблемами:

  • Мы стали писать тест-кейсы на каждый чих. То есть у нас были проверки в E2E-тесте для проверки того, что при нажатом чек-боксе отображается некоторый текст, а в выключенном состоянии текст другой. QA хотели проверить все кейсы и максимально обезопасить себя на стадии приемки, поэтому E2E-тесты начали походить на Unit-тесты с той лишь разницей, что на прогон таких тестов тратилось значительно больше времени и ресурсов.

  • Появилось большое количество тестов с повторяющимся флоу. Например, есть фича, в которой пользователь может заказывать справки. Пользователь попадал на экран Списка справок, выбирал справку, после чего открывался экран Настроек справки (выбор языка, даты и так далее). После мы попадали на экран Превью, где мы могли посмотреть PDF-файл нашей справки.

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

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

По итогу мы получали пирамиду тестирования в виде песочных часов. Мы делали много Unit- и E2E-тестов и практически не писали интеграционных тестов.

Почему это не круто?

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

Е2Е-тесты хорошо проверяют интеграцию между всеми компонентами приложения, но обходятся они достаточно дорого по следующим причинам:

  • Тесты запускаются на эмуляторах. Сам тест проходит долго, потому как он имитирует реальные действия пользователя (одна только авторизация длится около 30 секунд на каждом запуске теста).

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

  • Из-за того, что прогоны тестов проводятся на CI, иногда достаточно трудно понять, по какой именно причине сломался тест.

  • Е2Е-тесты имеют свойство падать без каких-либо изменений в коде (нестабильность интернета, нестабильность тестового фреймворка, проблемы на реальном устройстве и другие причины), именно поэтому их еще называют flaky tests.

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

Почему так?

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

Для E2E-тестов понятно, что на тестовом окружении должно быть поднято все, а для интеграционного только часть и не всегда понятно какая. Это приводило к тому, что всем было проще не разбираться и писать E2E тесты. Это негативно сказывалось на времени прогона тестов в целом.

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

Что мы сделали

Командный тренинг

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

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

Пользовательские истории

У нас было много обсуждений с QA, пока мы не пришли к формату тест-кейса, который должен показывать какую-то пользовательскую историю.

Небольшой пример

У нас есть фича, в которой пользователь заказывает себе справку по счету. Здесь имеется три экрана:

  • Экран списка справок.

  • Экран настроек справки (язык, период и так далее).

  • Экран превью справки.

Как бы это выглядело раньше?

Мы бы проверили Экран списка справок: UI-элементы, отправляемые запросы. После чего аналогичные проверки были бы на другие два экрана.

Здесь было две проблемы:

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

  • Было много запусков тестов на реальном устройстве, которые во многом дублировали себя.

Как это сделано сейчас?

Тест-кейс != Е2Е-тест.

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

Сам тест разделяется на шаги (steps). Обычно шаг это один экран внутри всего флоу (но бывают и другие ситуации). Мы пишем в description название этого шага, делаем скриншот и все проверки, которые касаются этого шага.

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

И есть кейсы, в которых проверяется какое-то конкретное действие, например отправка запроса при нажатии на кнопку. Для таких тестов достаточно написать Unit-тест либо интеграционный тест. Такие ошибки мы можем отлавливать на стадии пул-реквестов и исправлять их до того, как они попадут к QA.

Пирамида

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

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

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

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

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

  • Выделили отдельные Аndroid-модули для тестовых компонентов.

  • Подготовили базовые моки (сущности хранения сессий, базы данных и так далее).

  • Написали документацию по основным принципам тестирования с примерами.

Итоги

Мы пришли к осмысленной, правильной пирамиде тестирования.

  • Начали более сбалансированно распределять тесты по слоям.

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

  • Смогли задать направление для архитектурных изменений и упростить написание окружения вокруг тестов.

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

Подробнее..

Перевод Как импортировать существующие виртуальные машины VMWare в Terraform

11.12.2020 18:19:05 | Автор: admin

Будущих учащихся на курсе "Infrastructure as a code in Ansible", а также всех желающих приглашаем принять участие в открытом вебинаре на тему "Управление Kubernetes при помощи Kubespray".


И по устоявшейся традиции делимся с вами переводом полезной статьи.


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

Первоначально опубликовано в блоге techbeatly; там же доступны другие статьи по теме.

Читайте также:Программа обучения и советы по прохождению экзамена HashiCorp Certified Terraform Associate.

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

Шаг 1. Получение сведений о существующей виртуальной машине из VMWare vCenter

Выполните вход в VMWare vCenter и найдите данные виртуальной машины. Вам потребуются такие данные, какцентр обработки данных,папка виртуальной машины,имя виртуальной машины,ЦП,память,диски т.д.

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

Шаг 2. Создание кода Terraform для существующей виртуальной машины

В Terraform нет возможности импортировать существующую инфраструктуру в код Terraform. Вместо этого приходится писать конфигурацию Terraform, соответствующую имеющейся инфраструктуре, и импортировать ее.

Ниже приведен код Terraform, который я написал для виртуальной машины со следующим путем и именем:/DC1/vm/DEV/DEV2.

См. содержимое файлаvmware-import-vm.tf.

provider "vsphere" {  user           = var.vsphere_user  password       = var.vsphere_password  vsphere_server = var.vsphere_server  # If you have a self-signed cert  allow_unverified_ssl = true}data "vsphere_datacenter" "dc" {  name = "DC1"}data "vsphere_datastore" "datastore" {  name          = "datastore1"  datacenter_id = data.vsphere_datacenter.dc.id}data "vsphere_compute_cluster" "cluster" {  name          = "AZ1"  datacenter_id = data.vsphere_datacenter.dc.id}data "vsphere_network" "network" {  name          = "VM Network"  datacenter_id = data.vsphere_datacenter.dc.id}resource "vsphere_virtual_machine" "vm" {  name             = "DEV2"  resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id  datastore_id     = data.vsphere_datastore.datastore.id  wait_for_guest_net_timeout = 0  wait_for_guest_ip_timeout  = 0  # only if you DO NOT want to wait for an IP address  wait_for_guest_net_routable = false  num_cpus = 1  memory   = 2048  #guest_id = "other3xLinux64Guest"  network_interface {    network_id = data.vsphere_network.network.id  }    disk {    label            = "disk0"    size             = 20    thin_provisioned = false  }}

Я также объявил несколько переменных для передачи учетных данных VMWare.

$ cat variables.tf variable "vsphereuser" {}variable "vspherepassword" {}

В этом примере я передаю свои учетные данные VMWare vCenter с помощью переменных среды (см. пример ниже).

$ export TFVARvsphereuser='Administrator@lab.local'$ export TFVARvspherepassword='mypassword'

Шаг 3. Инициализация кода Terraform

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

$ terraform initInitializing the backend...Initializing provider plugins...- Finding latest version of hashicorp/vsphere...- Installing hashicorp/vsphere v1.24.2...- Installed hashicorp/vsphere v1.24.2 (signed by HashiCorp)The following providers do not have any version constraints in configuration,so the latest version was installed.To prevent automatic upgrades to new major versions that may contain breakingchanges, we recommend adding version constraints in a required_providers blockin your configuration, with the constraint strings suggested below.* hashicorp/vsphere: version = "~> 1.24.2"Terraform has been successfully initialized!You may now begin working with Terraform. Try running "terraform plan" to seeany changes that are required for your infrastructure. All Terraform commandsshould now work.If you ever set or change modules or backend configuration for Terraform,rerun this command to reinitialize your working directory. If you forget, othercommands will detect it and remind you to do so if necessary.

Проверим текущее состояние, управляемое Terraform:

$ terraform showNo state.

Да, мы пока еще не выполнили запуск.

Примечание. Что произойдет, если я выполню командуterraform apply? Все просто, система попытается выделить ресурсы для виртуальной машины, но эта попытка завершится ошибкой. Система сообщит, что виртуальная машина с именемDEV2уже существует. Как бы то ни было, в данном примере это не наш случай.

Шаг 4. Импорт виртуальной машины в состояние Terraform

Итак, теперь все готово к импорту имеющейся у нас виртуальной машины в состояние Terraform.

$ terraform import vsphere_virtual_machine.vm /DC1/vm/DEV/DEV2vsphere_virtual_machine.vm: Importing from ID "/DC1/vm/DEV/DEV2"...vsphere_virtual_machine.vm: Import prepared!  Prepared vsphere_virtual_machine for importvsphere_virtual_machine.vm: Refreshing state... [id=4219040f-5842-ba52-b7e4-cd9064c1f36c]Import successful!The resources that were imported are shown above. These resources are now inyour Terraform state and will henceforth be managed by Terraform.

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

Это можно проверить, выполнив командуterraform showеще раз.

$ terraform show# vsphere_virtual_machine.vm:resource "vsphere_virtual_machine" "vm" {    boot_delay                              = 0    boot_retry_delay                        = 10000    boot_retry_enabled                      = false    change_version                          = "2020-11-03T08:33:13.180937Z"    cpu_hot_add_enabled                     = false    cpu_hot_remove_enabled                  = false    cpu_limit                               = -1    cpu_performance_counters_enabled        = false    cpu_reservation                         = 0    cpu_share_count                         = 1000    cpu_share_level                         = "normal"    custom_attributes                       = {}    datastore_id                            = "datastore-13"    efi_secure_boot_enabled                 = false    enable_disk_uuid                        = false    enable_logging                          = true    ept_rvi_mode                            = "automatic"    extra_config                            = {}    firmware                                = "bios"    folder                                  = "DEV"    force_power_off                         = true    guest_id                                = "rhel7_64Guest"    guest_ip_addresses                      = []    hardware_version                        = 14    host_system_id                          = "host-12"    hv_mode                                 = "hvAuto"    id                                      = "4219040f-5842-ba52-b7e4-cd9064c1f36c"    ide_controller_count                    = 2    imported                                = true    latency_sensitivity                     = "normal"    memory                                  = 2048    memory_hot_add_enabled                  = false    memory_limit                            = -1    memory_reservation                      = 0    memory_share_count                      = 20480    memory_share_level                      = "normal"    migrate_wait_timeout                    = 30    moid                                    = "vm-47"    name                                    = "DEV2"    nested_hv_enabled                       = false    num_cores_per_socket                    = 1    num_cpus                                = 1    pci_device_id                           = []    poweron_timeout                         = 300    reboot_required                         = false    resource_pool_id                        = "resgroup-8"    run_tools_scripts_after_power_on        = true    run_tools_scripts_after_resume          = true    run_tools_scripts_before_guest_reboot   = false    run_tools_scripts_before_guest_shutdown = true    run_tools_scripts_before_guest_standby  = true    sata_controller_count                   = 1    scsi_bus_sharing                        = "noSharing"    scsi_controller_count                   = 1    scsi_type                               = "pvscsi"    shutdown_wait_timeout                   = 3    swap_placement_policy                   = "inherit"    sync_time_with_host                     = false    tags                                    = []    uuid                                    = "4219040f-5842-ba52-b7e4-cd9064c1f36c"    vapp_transport                          = []    vmware_tools_status                     = "guestToolsRunning"    vmx_path                                = "DEV2/DEV2.vmx"    wait_for_guest_ip_timeout               = 0    wait_for_guest_net_routable             = true    wait_for_guest_net_timeout              = 5    cdrom {        client_device  = false        datastore_id   = "datastore-13"        device_address = "sata:0:0"        key            = 16000        path           = "ISO/rhel-server-7.7-x86_64-dvd.iso"    }    disk {        attach           = false        controller_type  = "scsi"        datastore_id     = "datastore-13"        device_address   = "scsi:0:0"        disk_mode        = "persistent"        disk_sharing     = "sharingNone"        eagerly_scrub    = false        io_limit         = -1        io_reservation   = 0        io_share_count   = 1000        io_share_level   = "normal"        keep_on_remove   = true        key              = 2000        label            = "disk0"        path             = "DEV2/DEV2.vmdk"        size             = 20        thin_provisioned = false        unit_number      = 0        uuid             = "6000C29b-c4f0-764a-9054-a042931350c4"        write_through    = false    }}

Заключение

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

Подробные сведения см. в документации по ресурсу vsphere_virtual_machine.

Полный код приведен на GitHub в репозиторииterraform-vmware-demos.


Узнать подробнее о курсе "Infrastructure as a code in Ansible".

Зарегистрироваться на открытый урок на тему "Управление Kubernetes при помощи Kubespray".

ЗАБРАТЬ СКИДКУ

Подробнее..

Вжух, и прогоны автотестов оптимизированы. Intellij IDEA плагины на службе QA Automation

16.12.2020 12:18:50 | Автор: admin


Привет, Хабр. Я работаю QA Automation инженером в компании Wrike и хотел бы поговорить о том, как нам удалось оптимизировать процесс код-ревью для репозитория с 30 000+ автотестов при помощи IntelliJ IDEA плагина. Я расскажу о внутреннем устройстве плагина и о том, какие проблемы он решает в нашей компании. А еще в конце статьи будет ссылка на Github репозиторий с кодом плагина, с помощью которого вы сможете попробовать встроить плагин в ваши процессы.

Автотесты и деплой в Wrike


Мы пишем много разнообразного кода на Java в проекте автотестов. Сейчас у нас существует более тридцати тысяч тестов, которые тестируют наш продукт через Selenium, REST API, WebSocket и т.д. Все тесты разбиты на множество Maven-модулей в соответствии со структурой кода фронтенда. Проект активно меняется (релизы 1-3 раза в день), и, чтобы поддерживать качество кода, мы используем хорошо развитый механизм код-ревью. Во время ревью проверяем не только качество, но и работоспособность тестов.

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

Чтобы избежать таких ситуаций, мы решили добавить в процесс код-ревью одно действие. Автор merge request должен приложить ссылку на зеленый прогон автотестов в Teamcity, а ревьюер перейти по ней и проверить, что прогон действительно зеленый.

Какие проблемы могут возникнуть при запуске автотестов


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

У нас есть два механизма запуска тестов:

  1. По группам продуктовая разметка вида Epic/Feature/Story.
  2. По идентификаторам (id) любой автотест помечается уникальным числом, и можно запускать прогон по набору таких чисел.

В большинстве случаев прогон бежит по продуктовой группе тестов: указать группу быстрее, чем собирать id автотестов по всему проекту. Автоматизатор пытается по изменениям в коде предположить, автотесты каких частей продукта он затронул.

Дальше события могут развиваться по нескольким сценариям.

Сценарий 1: Запущено меньше тестов, чем в действительности затронуто новым кодом.

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

Сценарий 2: Запущено больше тестов, чем в действительности затронуто новым кодом.

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

Чтобы решить эти проблемы, мы создали Find Affected Tests IntelliJ IDEA плагин, который быстро и надежно находит список id тестов, затронутых изменениями в коде проекта автотестов.

От бизнес-проблем к реализации


Но почему именно IntelliJ IDEA плагин?, спросите вы. Чтобы создать инструмент, который решит наши проблемы, мне пришлось ответить на два вопроса:

  • Откуда брать изменения кода?
  • Как по изменениям в коде найти затронутые id?

На первый вопрос ответ был очевиден. Мы используем Git в качестве VCS и через него можем получать информацию про состояние текущей ветки.

Ответом на второй вопрос стал PSI (Program Structure Interface). Я выбрал именно этот инструмент по нескольким причинам:

  1. Мы используем IntelliJ IDEA в качестве IDE.
  2. IntelliJ Platform SDK включает в себя PSI удобный инструмент для работы с кодом проекта, который используется и самой IntelliJ IDEA.
  3. Функциональность IntelliJ IDEA можно расширить за счет механизма плагинов.

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

Структура плагина


Давайте посмотрим на UI плагина, чтобы представить, что видит автоматизатор перед запуском поиска id:


Так выглядит упрощенная версия, которая выложена на GitHub

Все интуитивно понятно: запустили поиск, посмотрели результат. Результат состоит из двух частей: id автотестов и списка Maven-модулей, в которых эти автотесты находятся (зачем нужен этот список я расскажу дальше в этой статье).

Внутренняя структура плагина состоит из двух основных модулей:

  • Git модуль получает информацию о текущей ветке и преобразует ее к формату, понятному PSI.
  • PSI модуль на основании данных от Git модуля ищет id тестов, которые затронуты изменениями в текущей ветке.

Взаимодействие UI-части плагина с модулями схематично выглядит так:


AffectedCodeUnit и DisplayedData это классы для передачи данных между модулями

Git модуль


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

Для каждого файла собираются следующие данные:

  • Имя файла, имя модуля и путь до файла относительно корня проекта. Это позволяет в дальнейшем упростить поиск файла через механизмы PSI.
  • Номера измененных строк в файле. Это позволяет понять, что именно было изменено в структуре Java-кода.
  • Был ли файл удален или создан с нуля. Это помогает оптимизировать работу с ним в будущем и избежать ошибок в некоторых ситуациях (например, не обращаться к удаленному файлу через PSI).

Все эти сведения содержатся в выводе команды git diff. Пример результата выглядит так:


В выводе команды можно сразу заметить путь до измененного файла. Данные о номерах строк измененного кода содержаться в строках вида @@ -10 +10,2 @@. В этой статье я не буду подробно объяснять их смысл, но примеры можете посмотреть на Stackoverflow или поискать информацию про git diff unified format

Аргументами для git diff выступают текущая локальная ветка пользователя и remote master, а ряд ключей позволяет сократить вывод команды до нужного размера.

Каким образом происходит взаимодействие с Git в коде плагина? Я попытался найти встроенные механизмы IntelliJ IDEA, чтобы не изобретать велосипед. В итоге обнаружил, что для Git в IntelliJ IDEA существует свой плагин git4Idea. По сути это GUI для стандартных операций с проектом. Для доступа к Git в нашем плагине используются интерфейсы из кода git4Idea.

В итоге получилась такая схема:



Через класс GitBranch запрашивается diff, затем diff отправляется в DiffParser. На выходе остается список объектов класса AffectedCodeUnit (в них содержится информация об измененных файлах с кодом). О судьбе этого списка я расскажу в описании PSI модуля.

PSI модуль


Теперь информацию об измененных файлах нужно применить для поиска id автотестов. Чтобы разобраться, как по номерам измененных строк найти элементы Java-кода, нужно подробнее посмотреть на устройство PSI.

Как IntelliJ IDEA работает с произвольным проектом. Для PSI любой файл преобразуется в дерево отдельных элементов. Каждый элемент реализует интерфейс PsiElement и представляет собой структурную единицу кода для конкретного языка программирования. Это может быть целый класс или метод, а может быть закрывающая скобка небольшого блока кода.

Для нашей задачи элемент Java кода и есть PsiElement-объект. Здесь мы приходим к новой формулировке вопроса. Как по номерам измененных строк кода найти PsiElement-объекты? В интерфейсе PsiFile (представление файла с кодом в виде объекта) был унаследован метод findElementAt, который по сдвигу относительно начала текстового представления файла умел находить PsiElement.

Но возникла новая проблема: каждая строка в файле потенциально содержит немало символов, и по номеру строки можно получить много разных PsiElement-объектов.

Как выбрать нужный PsiElement-объект. Дерево, в которое IntelliJ IDEA отображает Java-код, может состоять из огромного числа узлов. Конкретный узел может описывать незначительный элемент кода. Для меня важны узлы конкретных типов: PsiComment, PsiMethod, PsiField и PsiAnnotation.

И вот почему:

String parsedText = parser.parse("Text to parse");

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

public String parseWithPrefix(Parser parser, String prefix) {    String parsedText = parser.parse("Text to parse");    return prefix + parsedText;}

В такой ситуации нет смысла пытаться понять, что поменялось в строчке кода с объявлением String parsedText. Нам важно, что изменился метод parseWithPrefix. Таким образом, нам не важна излишняя точность при поиске PsiElement-объектов для измененной строки кода. Поэтому я решил брать символ посередине строки как измененный и искать привязанный к нему PsiElement. Такая процедура позволила получить список затронутых PsiElement объектов и по ним искать id автотестов.

Конечно, можно придумать примеры Java-кода с диким форматированием, в котором нужно учитывать больше условий для поиска PsiElement-объекта. Как в этом примере:

public String parseWithPrefix(Parser parser, String prefix) {    String parsedText = parser.parse("Text to parse");    return prefix + parsedText; } private Parser customParser = new Parser();

Изменения в последней строке могут затрагивать как метод parseWithPrefix, так и поле customParser. Но у нас есть механизмы анализа кода, которые не допустят подобное в мастер.

Как устроен базовый алгоритм получения id автотестов по набору PsiElement-объектов. Для начала нужно уметь по PsiElement-объекту получать его использования (usages) в коде. Это можно сделать с помощью интерфейса PsiReference, который реализует связь между объявлением элемента кода и его использованием.

Теперь сформулируем краткое описание алгоритма:

  1. Получаем список PsiElement-объектов от Git модуля.
  2. Для каждого PsiElement-объекта ищем все его PsiReference объекты.
  3. Для каждого PsiReference объекта проверяем наличие id и сохраняем, если нашли.
  4. Для каждого PsiReference объекта ищем его PsiElement-объект и рекурсивно запускаем на нем описанную процедуру.

Процедуру следует повторять, пока находятся PsiReference-объекты.

Для задачи поиска абстрактной информации в коде алгоритм выглядит так:



В нашей задаче нужно найти id автотестов. Вот так можно применить блок-схему для этого:



Оптимизация алгоритма. В процессе реализации алгоритма я столкнулся с одной проблемой. Алгоритм работает с PsiTree-объектом, который состоит из большого числа узлов. Каждый узел соответствует элементам Java-кода. Механизм поиска PsiReference-объектов по умолчанию может находить довольно незначительные элементы Java-кода, из-за чего алгоритм будет совершать много лишних действий в процессе движения по дереву. Это замедляет работу алгоритма на несколько минут в тех случаях, когда найдено около тысячи тестов или больше.

Пример для наглядности: предположим, что значение переменной wrikeName изменилось:

public List<User> nonWrikeUsers(List<User> users) {    String wrikeName = "Wrike, Inc.";    return users.stream()                     .filter(user -> !wrikeName.equals(user.getCompanyName()))                     .collect(Collectors.toList());}

Тогда в поисках использования wrikeName мы сначала придем к вызову метода equals, затем к отрицанию результата вызова equals, а после к лямбде внутри вызова filter. Но эту длинную цепочку шагов можно заменить на поиск использований изменившегося метода nonWrikeUsers.

И тогда вместо четырех итераций алгоритма по каждому узлу дерева мы получим всего одну:



Проблему лишних операций в алгоритме удалось решить благодаря тому же ограничению типа данных PsiElement-объектов до PsiComment, PsiMethod, PsiField и PsiAnnotation. Именно эти PSI-сущности содержат всю релевантную информацию для поиска id затронутых автотестов.

Пример релевантной информации: затронуто поле какого-то класса (объект типа PsiField), оно могло использоваться в каком-то автотесте.

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

В начало логики алгоритма я добавил поиск прародительского узла с нужным типом данных. Поиск осуществляется при помощи метода getParentOfType класса PsiTreeUtil. Для каждого типа PsiElement-объекта я создал реализацию логики обработки. Например, для прародителя типа PsiField отрабатывает объект класса FieldProcessing, а для PsiComment объект класса CommentProcessing. Код плагина исполняет подходящую логику в зависимости от результата работы метода getParentOfType.

В общем виде логика работы выглядит так:



Пример реализации логики обработки одного из типов PsiElement-объектов:



Подытожу основные основные особенности PSI модуля:

  1. Модуль работает по рекурсивному алгоритму поиска id автотестов. Краткая идея алгоритма заключается в поиске id для всех измененных PsiElement-объектов через механизм PsiReference.
  2. Алгоритм работает не со всеми узлами PsiTree-объекта, чтобы не совершать избыточные итерации.

Дополнительные возможности плагина


После того, как мы начали использовать плагин в работе, коллеги прислали несколько запросов по его улучшению. Это помогло усовершенствовать инструмент и оптимизировать прогоны автотестов.

Делаем работу с Git модулем гибче


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

Пример 1: автоматизатор создает merge request с одним коммитом. Он прогоняет 1000 тестов, которые затронуты этим коммитом. Ревьюер оставляет замечания по коду, автор merge request их исправляет. Эти правки затрагивают теперь только 200 тестов, но плагин предложит прогнать и 1000 тестов из изначального прогона, так как учтен первый коммит. Получаем лишние тесты для прогона.

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

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

Решение: я реализовал функциональность для сравнения локальной версии ветки с произвольной веткой (в общем виде с любым коммитом). Это позволяет гибко учитывать коммиты при работе с несколькими ветками.

Помогаем внедрить новую оптимизацию с помощью плагина


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

Тогда мы решили изменить сценарий запуска. Теперь пересобираются только модули, указанные вручную. Это позволило в среднем снизить время компиляции с 20 минут до 3-4.

Чтобы узнать, в каких именно модулях находятся затронутые тесты, я добавил в плагин логику вывода затронутых модулей. Для ее реализации во время получения id автотеста нужно взять пути до файла с тестом в операционной системе и относительно корня проекта. Зная структуру директорий типичного Maven проекта, можно обрезать все лишнее и оставить часть пути с именами модулей.

В итоге удалось быстрее внедрить новую логику запуска и как результат уменьшить время ожидания прогона тестов и КПД инфраструктуры.

Планы по развитию плагина


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

  1. Открыть нужный билд в Teamcity.
  2. Ввести данные для прогона.
  3. Сохранить ссылку на прогон для проверки его результатов.

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

Так возникла задача: научить плагин запускать прогон в Teamcity самостоятельно. У нас есть специальный инструмент, который аккумулирует большинство данных, связанных с CI/CD процессами. Поэтому плагину проще отсылать запрос на запуск прогона этому инструменту (в нем же будет сохранена ссылка на прогон). Реализация этой логики позволит уменьшить количество рутинных действий после написания кода и сделает процесс запуска автотестов надежнее.

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

Идея состоит в том, чтобы несколько потоков одновременно занимались анализом разных узлов Psi-дерева. Для этого нужно грамотно выстроить работу с общими ресурсами (например, со множеством уже обработанных PsiElement-объектов). Одним из возможных решений может быть Java Fork/Join Framework, так как работа с деревом подразумевает рекурсивность. Но здесь есть подводные камни: у IntelliJ IDEA есть свой пул потоков, в который нужно грамотно встроить новую логику.

Несколько выводов


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

У нас есть сборка в Teamcity, в которой традиционно гоняются запущенные вручную автотесты. Она позволяет запустить прогон с указанием Maven-модулей. Я выбрал эту сборку для примера, потому что ссылку именно на нее автор merge request прикладывает во время код-ревью. А еще запуски в этой сборке выполняются вручную, и автоматизатор может взять набор id для запуска только из плагина (вряд ли кто-то в здравом уме будет собирать их руками).

Так выглядит график общего количества запусков в этой сборке:



В марте этого года (точка 03.2020) мы анонсировали сборку официально, а в апреле (точка 04.2020) я добавил в плагин вывод Maven-модулей, в которых находились затронутые автотесты. После этого количество запусков возросло в несколько раз. Следующий скачок произошел в сентябре, так как эта сборка стала использоваться для перезапуска автотестов в нашей внутренней деплой туле, но это нам не так интересно.

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



За последние полгода порядка 70-80% запусков автотестов происходят с указанием списка id: коллеги активно используют плагин по назначению. В апреле мы добавили в плагин вывод Maven-модулей. На графике видно, что это увеличило процент запусков с id с 50% до 75% за пару месяцев.

Действительно ли прогоны по id быстрее прогонов по продуктовой разметке? Статистика среднего времени прогона для данной сборки показывает, что да. Для id мы получаем примерно в три раза меньшее время, чем для продуктовой разметки: вместо условных 25-30 минут 8-10 минут.

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

Пишите в комментариях про ваши успешные или не очень успешные примеры автоматизации.

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

Всем добра и побольше эффективных оптимизаций!
Подробнее..

Автоматизация тестирования мобильных приложений. Часть 2 предусловия, верификация элементов и независимость шагов

16.03.2021 18:09:12 | Автор: admin

Меня зовут Дмитрий Макаренко, я 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)  endend

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

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

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

Метод 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}")  endend

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)  endend

Мы ждём, пока переключатель (элемент 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)  endend

Мы используем 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  endend

Используя метод 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" GIFsAnd   primary_user sends 7th GIF in the listThen  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?  endend

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

  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_listend

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

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?  endend

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

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  endend

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

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

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  endend

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

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

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

Практика 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)  endend
class WaitForReplyAlert < AppAlertAndroid  def verify_alert_lexemes    verify_alert(title:        WaitForReply::TITLE,                 description:  WaitForReply::MESSAGE,                 first_button: WaitForReply::CLOSE_BUTTON)  endend
class SpecialOffersAlert < AppAlertAndroid  def verify_alert_lexemes    verify_alert(description:  SpecialOffers::MESSAGE,                 first_button: SpecialOffers::SURE_BUTTON,                 last_button:  SpecialOffers::NO_THANKS_BUTTON)  endend

Во всех примерах мы вызываем метод verify_alert, передавая ему лексемы для проверки необходимых элементов. При этом, как вы можете заметить, WaitForReplyAlert мы не передаём лексему для второй кнопки, так как её не должно быть, а 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_buttonend

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

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

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

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

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)  endend

Реализация метода 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

Подробнее..

Документирование микросервисов в LeanIX (EAM)

08.07.2020 12:13:32 | Автор: admin


Расскажу о нашем опыте автоматического документирования 150+ микросервисов в системе LeanIX Enterprise Architecture Managment. Многое получилось, как мы и хотели, для чего-то пришлось делать специальные доработки, часть вопросов не смогли решить. Но в любом случае мы получили опыт и готовы им поделиться.


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


Для чего вообще нужно описывать ландшафт предприятия?


Короткий ответ для поддержания процессов управления архитектурой предприятия (Enterprise Architecture Management / EAM).


Более полный ответ уже заключен в самой дисциплине EAM это системная концепция для разработки и развития IT-ландшафта в соответствии с бизнес-возможностями и процессами, а также оргструктурой организации. Бизнес-возможности (business capabilities) в свою очередь являются основой, на которой предприятие строит свои текущие и будущие бизнес-процессы.


EAM включает в себя набор сценариев использования каталога IT-систем, вот основные:



Cценарии выше релевантны не только для архитекторов предприятия (Enterprise Architect), но и для системных архитекторов (System Architect) и архитекторов решений (Solution Architects). Сам по себе каталог IT-систем особенно полезен на этапе интеграции приложений и отслеживания зависимостей. Тем более, что в большинстве организаций такой каталог является "Single Point of Truth".


Актуальность такого каталога необходимо постоянно поддерживать, либо тратя на это много времени сил, либо автоматизируя наполнение.
Эта проблема особенно ощутима, если хотя бы часть ландшафта предприятия построена на микросервисной архитектуре. В этом случае каталог будет содержать уже не 100-500 систем, а 500-1000 (и даже большое) компонентов.



Вот и у нас есть устоявшийся ландшафт на 500+ IT-систем (на диаграмме разбивка на отделы), и уже сейчас поддержка единой базы IT-приложений требует существенных трудозатрат (каталог самих приложений, их связей с бизне- функциями, связи между собой, roadmap каждого приложения и т.п.).


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


Соответственно, возникает вопрос как с этим всем жить?


  • Самое простое решение не документировать в центральном каталоге каждый микросервис. Такой вариант не является совсем уж неверным: сами микросервисы не настолько интересны для бизнес-анализа ландшафта, а группы микросервисов можно объединить в "приложения"/"системы"/"домены" и документировать уже приложения. У этого решения, естественно, есть очень приятный плюс "сокращение трудозатрат" поддержки EAM каталога. Но если каждая система в каталоге будет представлять собой "черный ящик", то теряется прозрачность работы и взаимодействия внутренних компонентов. Также каждая IT-система в рамках своих проектов должна изобретать способ документирования своего каталога микросервисных компонентов, потому что при разработке все равно необходим общей список всех микросервисов приложения.


  • Второе решение автоматизировать документирование микросервисов в каталоге IT-ландшафта. Вот с этим мы и поэкспериментируем.



Текущий ландшафт у нас описан в системе, построенной на платформе Alfabet. Сейчас мы активно анализируем EAM решение от компании LeanIX как потенциального преемника.


LeanIX относительно молодой продукт, поэтому с моей точки зрения, в нем пока мало "унаследовательности", и он достаточно гибок и обладает современными возможностями для интеграции, и что немаловажно, опять же по моему мнению, у него довольно удобный и приятный пользовательский интерфейс.
Даже Gartner в 2019 году назвал LeanIX одним из Visionaries.



Можно долго перечислять причины выбора именно LeanIX (мы действительно проводили RFI/RFP процесс), но не надо забывать что одним из основных инвесторов компании LeanIX является Deutsche Telekom Capital Partners, то есть мы сами заинтересованы в развитии продуктов LeanIX.


Разработчики системы LeanIX EAM изначально заложили в продукт возможность документирования микросервисов. Попытались они это сделать на основе подхода, предложенного проектом Pivio ( http://pivio.io/)


Сам по себе "Pivio-подход" работает следующим образом: рядом с исходным кодом компонента (микросервиса) размещается yaml файл с метаинформацией о компоненте, который
посредством Pivio Client загружается в Pivio Server.



Cервер хранит информацию в ElasticSearch и предоставляет API для ваших инструментов.
Также существует Pivio Webview c очень ограниченными возможностями поиска и визуализации каталога (видимо, это как раз та часть проекта Pivio, которую вы должны были сами расширять для своих потребностей).


Вызов Pivio Client может быть частью процесса сборки или деплоймента компонента.


Естественно, для загрузки документов в Pivio Client они должны быть определенного формата. (см. http://pivio.io/docs/)


Под спойлером пример такого документа:


Скрытый текст
id: next-generation-print-2342-2413-9189-1990name: Next Generation Print Serviceshort_name: NGPStype: serviceowner: Team Goldfingerdescription: Prints all kinds of things. Now with 3D printing support.vcsroot: git://git.vcs.local/UBPcontact: Auric Goldfingerlifecycle: productiontags:- Old- Demolinks:homepage: http://wiki.local/ubpbuildchain: http://ci.local/ubpapi_docs: http://docs.local/ubp-apiservice:provides:- description: REST APIservice_name: uber-bill-print-serviceprotocol: httpsport: 8443transport_protocol: tcppublic_dns:- api.demo-company.com- soap.demo-company.io- description: SOAP API (legacy)service_name: print-serviceprotocol: httpsport: 80transport_protocol: tcpdepends_on:internal:- service_name: print-servicewhy: need to print- service_name: gateway-service- short_name: NGPSport: 8719external:- target: https://api.superdealz.me:443transport_protocol: tcpvia: proxy-servicewhy: Need to sync data with it.- target: mqtt://192.xxx.xxx.xxx:5028transport_protocol: tcpwhy: Get the latest Dealz.

От общей архитектуры Pivio, система LeanIX переиспользовала только PivioClient и формат файла (LeanIX Metadata как раз и есть файл в формате Pivio). В качестве каталога микросервисов и визуализации (поиск, отчеты, диаграммы) используются возможности самого LeanIX EAM.



Практика


Вот эту схему интеграции мы и стали проверять на своем проекте. Проект достаточно крупный: 15+ команд разработки, 150+ микросервисов уже в эксплуатации. Нам повезло, метаинформацию о своих микросервисах мы уже хранили вместе с кодом (у нас multirepo схема git-репозиториев, и в каждом репозитории есть метаинформация о компоненте).


Формат нашей метаинформации (https://github.com/AOEpeople/vistecture) отличается от Pivio, но это все еще yaml файл и преобразовать его "на лету" в pivio формат не составило труда.
Информация об интерфейсах и зависимостях между компонентами у нас также была уже задокументирована в машиночитаемом формате (мы используем её для настройки API Gateway).
Для начала мы решили не копировать всю информацию из swagger в систему EAM (структуру данных, методы и т.п.), это осталось пока за рамками, но будет задачей на будущее.


В планах было все просто: встраиваем в наш CI/CD pipeline дополнительный шаг по загрузке метаинформации в LeanIX (конвертация, вызов Pivio Client, который доступен как в качестве jar файла, так и в виде docker контейнера). Выглядело в планах все отлично: декларативное описание компонента, полная автоматизация обновления информации в EAM.



Документация LeanIX по использованию Pivo Client тут: https://dev.leanix.net/docs/microservices-based-on-yaml-files


Но к середине первого дня работы к нам пришло осознание, что все будет не так просто:


  • Оказалось, что LeanIX поддерживает только часть возможностей Pivio-формата (не все атрибуты).
  • Что еще страшнее, оказалось, что реализация Pivio в LeanIX не поддерживает описание интерфейсов между микросервисами (мы не можем документировать зависимости между компонентами).
  • Естественно, наш способ документирования не включал в себя случай, когда мы выводим сервис из эксплуатации и его надо удалить (нет у нас пока pipeline на удаление сервиса). Да и Pivio Client не умеет удалять описание микросервисов из LeanIX.

Пункт 1. решился дополнительным вызовом REST интерфейса самого LeanIX. (см. https://eu.leanix.net/services/pathfinder/v1/docs/#/)


Пункт 2. решился использованием GraphQL API LeanIX.


  • позволяет создать сущность "Интерфейса" и связать его c провайдером и потребителем.

Скрытый текст

Creating Interface


mutation {   createFactSheet(validateOnly: false,     input: {name: service["name"], type:Interface},     patches: [{op: replace, path:"/description",value: service_url },         {op: replace, path:"/externalId",value:"externalId:service["name"]"}] ) {                 factSheet {                     id                     name                     displayName}        }}

Attach interface to provider


mutation{  upsertRelation(from:      {externalId: {type: externalId, id: service["name"] }},           to: {externalId: {type: externalId, id: service["provider"]}},            type: relInterfaceToProviderApplication) {                relation {                id            }      }}

  • Но graphQL API LeanIX не декларатильвен, значит, надо выяснять разницу между тем что есть в LeanIX и тем, что мы хотим создать/обновить.

Скрытый текст

Searching if Interface/Microservice already exist:


{allFactSheets(     filter: {externalIds: ["externalId/" + externalId]}        ) {        edges {            node {                id                name                }            }      }}

  • Также мы столкнулись с тем, что нельзя привязать интерфейс к микросервису, если сам микросервис еще не документирован, следовательно, это требует последовательного создания документации на провайдера, потребителя и интерфейсов между ними. Решили мы пока эту задачу "топорно": создаем документацию на все компоненты и только потом документируем интерфейсы и зависимости.

Пункт 3. пока решили оставить как ручной шаг.


В итоге получилась такая рабочая схема:


Тут крупнее


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


Что это нам дало:


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

Тут крупнее


На схеме уже видно, что баланс разделения ответственности между командами в области домена order куда лучше, чем, например, в presales. В order большая часть работы выполняется всего двумя командами и затраты на коммуникации и интеграции значительно ниже, чем между 4-мя командами в presales.


  • Возможность полуавтоматически визуализировать потоки коммуникации.

    Тут крупнее
    На схеме процесс сбора бизнес метрик, который позволяет оценить прогресс наших бизнес-процессов.


  • Возможность визуализировать зависимости между сервисами.

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



Итоги и открытые вопросы:


  • Вопрос о том, нужно ли вообще документировать микросервисы в общем IT-ландшафте все еще остается открытым, так как пользы от такой детальной документации для главного пользователя EAM (Enterprise Architect / Business Architect) пока немного. Но мы показали, что такая подробная документация нам почти "ничего не стоит".
  • Автоматизация документирования отлично вписывается в pipeline CI/CD
  • Сама система LeanIX гибка и предоставляет широкие возможности для интеграции. Но это и ее минус, так как не все способы одинаково функциональны и требуется разобраться во всех них.
    • Интеграция с Pivio Client декларативная, выглядит как то, что надо, но не поддерживает описание интерфейсов и у нее нет логики по "удалению" документации из LeanIX.
    • В REST API легко делать простые операции, но любой поиск по критериями превращается в "танцы с бубном" на стороне клиента.
    • GraphQL API не всегда логичный (к этому, наверное, можно привыкнуть).
    • Integration API до него пока не добрались, потому что требуется разбираться с проприетарном форматом обмена данными (LDIF)
  • LeanIX молодая система, и в процессе проверки нашего подхода было найдено несколько дефектов. Надо отдать должное команде LeanIX: они исправляли проблемы быстро.
  • Нам имеет смысл пересмотреть какие API мы действительно хотим видеть в общем каталоге. Вероятно, что у нас будет три типа API:
    • internal в рамках подсистемы/домена;
    • system для поддержки процессов через несколько доменов;
    • enterprise уровень для интеграции внешних систем.

Cледующие шаги:


  • Хочется задокументировать swagger описания интерфейсов в той же системе (пока они у нас отдельно все собраны) или хотя бы просто проставить ссылки на наш API Developer Portal / Swagger UI.
  • Вероятно, придется настроить интеграцию через "Integration API", так как по описанию он наиболее полный с точки зрения автоматизации документирования.
  • После демонстрации нашего решения и результата компании LeanIX нам было предложено попробовать новый продукт "LeanIX Cloud Native Suite: Microservices Intelligence", который имеет метамодель более ориентированную на описание микросервисной архитектуры. Кроме вышеописанных интеграционных подходов также обещают прямую интеграцию с kubernets, что позволит снимать часть актуальной информации прямо с боевого окружения. Возможно, это станет темой следующей статьи.

Немного и полезных ссылок на прощание:


Подробнее..

Перевод Почему нужно автоматизировать эксплуатацию

04.03.2021 12:16:30 | Автор: admin

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

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

Gartner, 2019-2021 I&O Automation Benchmark Report

Пять преимуществ автоматизации инфраструктуры и эксплуатации

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

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

  3. Гибкость. Решения в сфере автоматизации часто предоставляют прямые интеграции и малокодовый доступ к API, что позволяет командам эксплуатации быстро развёртывать и внедрять новые технологии, либо создавать новые кросс-платформенные процессы без написания скриптов.

  4. Прозрачность. Автоматизируя мониторинг процессов и систем, команды эксплуатации могут собирать данные для оптимизации процессов и ресурсов, отслеживать и улучшать SLA за счёт перенаправления ресурсов при угрозе избыточного роста нагрузки, а также автоматически восстанавливать системы после сбоев.

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

Создание основы для современной эксплуатации

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

Gartner, Predicts 2019: IT Operations

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

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

Оркестрация и инфраструктура-как-код

Универсальные коннекторы, прямые интеграции и возможности бесшовно исполнять API (.NET-сборки, RESTful-сервисы, командные строки, хранимые процедуры и т.д.) всё это лежит в основе оркестрации, то есть способности централизованно интегрировать и управлять практически любым процессом.

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

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

AIOps

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

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

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

Гиперавтоматизация

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

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

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

NoOps будущее ИТ-эксплуатации?

Такие технологии, как AIOps, облачные вычисления (SaaS/IaaS) и гиперавтоматизация позволяют ИТ-департаментам сильно уменьшать количество ручных процессов и делают возможными полностью автоматизированные автономные среды.

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

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

Deloitte, NoOps In A Serverless World

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

Как сказал один из авторов статьи в Deloitte: NoOps на самом деле недостижима, но это эффективный лозунг.

Подробнее..

Приглашаем на Live-Вебинар Автоматизация процессов с GitLab CICD 29 Окт., 1500 -1600 (MST)

06.10.2020 12:23:44 | Автор: admin

Расширяем знания и переходим на следующий уровень.




Вы только начинаете изучать основные принципы Continuous Integration / Continuous Delivery или написали уже не один десяток пайплайнов? Вне зависимости от уровня Ваших знаний, присоединяйтесь к нашему вебинару, чтобы на практике разобраться, почему тысячи организаций по всему миру выбирают GitLab в качестве ключевого инструмента для автоматизации IT процессов.

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

В этом вебинаре мы разберем:
  • Основные элементы автоматизации в GitLab CI/CD
  • Принципы Pipelines-as-Code
  • AutoDevOps автоматический полноценный CI/CD конвейер, который самостоятельно автоматизирует весь процесс
  • Расширенные настройки и оптимизацию GitLab CI/CD

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

Cucumber и BDD, пишем ui авто тесты на iOS

08.10.2020 12:16:26 | Автор: admin

Предисловие

Привет, Хабр! В данной статье-мануале я хочу рассказать о базовых функциях такого фреймворка как Cucumber и его применение для создания ui авто тестов на мобильных iOS устройствах.

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

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

Несколько слов о Gherkin, Cucumber и BDD

Gherkin представляет из себя структурированную манеру написания документации для PO, бизнес-аналитиков и тестировщиков. Академическое определение Gherkinговорит, что это человеко-читаемый язык для описания поведения системы, каждая строчка начинается с одного из ключевых слов (Given-When-Then-And) и описывает одно из предусловий/шагов/результатов.

Cucumber - это инструмент, используемый для выполнения behavior driven (BDD) сценариев, написанных на языке Gherkin.

BDD (behavior driven development) - методология разработки ПО, основной идеей которой является составление требований к продукту в формате понятых не-специалисту поведенческих сценариев. Пример BDD сценария:

Scenario: Login with PINGiven the app is runningAnd I'am registered userAnd I see Login screenWhen I enter 4-digits PINThen I am logged in

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

Разворачиваем Cucumber

На сайте Cucumber поддержка iOS находится в любопытном статусе semi-official (полу-официальный?) поэтому процесс установки и настройки фреймворка местами весьма нетривиален.

  • Cucumber - это СocoaPod библиотека, поэтому начинаем с нее. Открываем терминал и устанавливаем CocoaPod

    sudo gem install cocoa pods

  • переходим в свой проект и создаем подфайл

    pod init

  • заходим в созданный подфайл. Проверяем, что в нем есть следующее:

source 'https://github.com/CocoaPods/Specs.git'platform :ios, '11.0'use_frameworks!inhibit_all_warnings!def test_pods  pod 'Cucumberish'endtarget НАЗВАНИЕ_ВАШЕГО_ПРОЕКТАCucumberTests do  test_podsend

Если этой записи нет - добавляем её руками, предварительно удалив всё из файла. Обратите внимание, что в таргете фигурирует название вашего проекта

  • устанавливаем XCFit

    sudo gem install xcfit

  • открываем Xcode, добавляем к проекту новый таргет, используя появившийся шаблон cucumberish bundle. В проекте появятся папка НазваниеВашегоПроектаCucumberTests с дефолтными файлами. Там же необходимо самостоятельно создать папку Features

  • идем в build phases таргета CucumberTests, удаляем дубликаты Copy Bundle Resources, Compile Sources и Link Binary With Libraries

  • закрываем Xcode, устанавливаем поды

    pod install

  • в проекте появится .xcworkspace файл, открываем его. Зачастую на этом шаге отваливаются стандартные файлы из папок Screens, Step Definitions и Supporting Files. Если это произошло - добавляем их обратно руками через Add Files to.

    Почти готово, мы в шаге от завершения установки Cucumber!

  • на момент написания этой статьи (осень 2020) из коробки фреймворк не собирается из-за нескольких ошибок в коде. Возможно, эти проблемы будут неактуальны. Через поиск в Xcode находим и правим следующие строки:

    • добавляем @objc перед class func CucumberishSwiftInit()

    • зменяем var elementQurey на var elementQuery

    • заменяем expectedMenuCount: uInt = uInt(menuCount)! на expectedMenuCount: Int = Int(menuCount)!

    • заменяем expectedCellCount: uInt = uInt(cellCount)! на expectedCellCount: Int = Int(cellCount)!

Вот теперь все. Жмем +U для запуска тестов. Если все сделано правильно, проект соберется, а в логе мы увидим сообщение Tests successfully executed 0 from 0. Это значит, что фреймворк успешно отработал, но не запустил ни одного теста, что не удивительно, так как мы еще не написали ни одного сценария.

Фреймворк "из коробки"

По умолчанию Cucumber имеют следующую структуру:

  • папка Features - в ней хранятся файлы с разрешением .feature, они содержат в себе один или несколько BDD сценариев. Каждый сценарий, написанный на Gherkin, состоит из набора строк, последовательно выполняемых при работе теста

  • <название проекта>CucumberTests.swift файл в корне фреймворка. Содержащий код, выполняемый для Given шагов, задает предусловия каждого теста. По умолчанию файл будет содержать шаг для запуска приложения. Также файл содержит ряд настроек, к примеру, управление тэгами, но о них чуть позже

  • папка Screens - в ней хранятся .swift файлы, содержащие локаторы, по которым можно обращаться к инспектируемым элементам приложения

  • папка Step Definitions - в ней хранятся .swift файлы содержащие код, выполняемый в шагах тестов

  • папка Common - в ней хранится commonStepDefinitions.swift файл, содержащий в себе код, который может выполняться для простых проверок типовых элементов (наличие объектов на экране, нажимаемость кнопок и т.п.)

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

Что происходит при запуске теста?

Фреймворк установлен, теперь мы хотим создать свой первый тест. Для того, чтобы это было проще сделать, надо понимать, что будет происходить c Xcode после того, как вы нажмете +U.

А происходить будет следующее:

  • фреймворк соберет проект на выбранном девайсе/эмуляторе

  • фреймворк построчно распарсит первый сценарий из первого фича-файла

  • все первые Given и And строки будут рассматриваться как предусловия теста. Они могут быть как совсем простыми и состоять из единственного Given условия, так и комплексными Given-And-And- наборами строк

Простой шаг-предусловиеПростой шаг-предусловие
  • фреймворк отправится в корневой swift файл за кодом, выполняемым в этих шагах. Если точнее, то для простого предусловия выше будет выполнен следующий код:

Шаг The app is running является первым и обязательным почти для любого сценария, так как в нем происходит запуск трестируемого приложения.

  • как только будут пройдены все Given шаги и дело дойдет до первой When или Then строки, предусловия будут считаться выполненными. За кодом для выполнения последующих строк фреймворк будет обращаться в .swift файлы из папки Step Definitions

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

Пишем свой первый тест

Итак, имея базовое представление о том, как работает фреймворк мы понимаем, что именно должно быть сделано для создания нашего первого теста:

  • создаем feature файл в папке Features. В начале файла обязательно должна быть общая информация описывающая функционал по шаблону:

Feature:In order to As a I want to 
  • пишем тестовый сценарий в feature файле, используя синтаксис Gerkin

  • в корневом swift файле раскрываем Given шаги этого сценария

  • в step definition файле раскрываем остальные шаги

Особенности написания Given шагов

Упомянутого выше предусловия the app is running самого по себе почти всегда будет недостаточно. Если точка входа в тест находится не на стартовой страничке, а где-то глубже, то удобно использовать комплексные Given-And-And- предусловия. При помощи еще одной или нескольких строк, мы можем привести приложение к нужному для конкретного теста состоянию.

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

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

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

  • запускаем приложение

  • если после запуска мы видим страницу регистрации, то регистрируемся, нажав на кнопку Get started. Ждем, пока прогрузится экран для установки пинкода, задаем пин 0000, вводим его повторно для подтверждения регистрации

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

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

  • Проверяем, что после регистрации/логина приложение прогрузилось, и мы находимся на экране My credentials. Простой ассерт, в котором мы ожидаем экран

  • Проверяем, что на экране в табличке credentialList нет сущностей, если они остались с предыдущих тестов, удаляем их одну за одной. Для этого тапаем по первой ячейке в таблице, вызываем меню действий, тапаем по кнопке "Delete" и подтверждаем кнопкой "Delete credential". Повторяем эти действия пока первая ячейка в таблице не перестанет существовать

В результате предусловия выполнены. Мы имеем запущенное приложение, ожидающее дальнейших шагов на пустом экране My credentials.

Несколько слов о тестовых шагах и CommonStepDefinitions

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

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

Допустим, в вашем приложении есть условный объект Menu, на нем находятся две стандартные кнопки Confirm и Deny, а также текст Please confirm. Для того, чтобы проверить отображение этого объекта, кнопок и текста, не обязательно в step definitions детализировать код для четырех разных шагов. Достаточно просто в сценарии написать эти шаги в подходящем под CommonStep формате:

And I see the "Menu" viewAnd I see "Confirm" buttonAnd I see "Deny" buttonAnd I see "Please confirm" text

При выполнении теста все эти строки будут определены как common шаги, а точнее, как один единственный шаг:

Использование common шагов позволяет здорово сократить количество кода, необходимого для простых проверок или действий. Но надо понимать, что этот метод не всегда подходит для тестирования кастомных элементов из-за сложных путей их обнаружения. В этом случае удобнее всего пользоваться обычными step definitions и обращаться к этим элементам через accessibility identifier.

Запускаем тесты

После того, как один или несколько BDD сценариев написаны и ко всем их строкам существует definition, можно запускать тестовый прогон комбинацией +U. В случае, если необходимо запустить только часть тестов, можно воспользоваться тегами, отметив ими желаемые сценарии в фича файлах (или фича файлы целиком) и перечислив теги для запуска в executeFeatures корневого .swift файла.

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

Из этого можно извлечь пользу. К примеру, на нашем проекте из общего количества авто-тестов в 50+ штук необходимо всего 5 тестов на регистрацию. После прохождения этих 5-и тестов состояние регистрации сохраняется и последующие 45 тестов начинаются уже с логина, минуя десяток тапов по кнопкам и пару экранов. Тем самым, мы экономим примерно 10 секунд времени от каждого теста, не выполняя одни и те же действия, уже протестированные ранее.

Обратная сторона медали - периодически требуется очищать приложение от различных сущностей в предусловиях тестов (как было показано выше на примере шага I have no credentials). Для этого приходится писать довольно объемные шаги, но в моем случае такой существенный выигрыш времени стоит того.

Отчет

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

В случае падения теста тут же можно будет увидеть подробный лог и скриншоты. Этот инструмент очень удобен при написании и отладке тестов на новые фичи.

Когда начинается магия?

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

Одна из самых полезных фишек кукумбера - переиспользование отдельно взятых шагов в любом месте, где это допустимо. Максимальное дробление сценариев на мельчайшие части, так раздражавшее поначалу, спустя 3-4 спринта привели к формированию внутри фреймворка большого пула разных шагов. Все эти предусловия, действия и результаты при грамотном написании должны работать независимо друг от друга и иметь одинаковые точки входа-выхода, в этом случае их можно переиспользовать, комбинировать и на лету собирать из них новые авто-тесты просто написав BDD-сценарий. В моей текущей работе бывали случаи, когда создание авто-тестов на новый функционал просто сводилось к написанию BDD сценария из уже существующих шагов. Новый авто-тест без программирования? Фантастика? Так вот с cucumber это действительно возможно.

Если добавить к этому то, что при разработке по BDD методологии сценарии для вас пишет PO и пишет их точь-в-точь как вам надо, то получается совсем утопическая вселенная, где тестировщику остается только скопипастить текст из Jira в фича-файл и получить работающий автотест.

Естественно, реальность всё-таки вносит свои корректировки, и PO будет иметь отличный от тестировщика взгляд на описание фичи, чаще всего, сценарии приходится глубоко редактировать, дополняя деталями или пропущенными шагами, примерять на них стандартные техники тест дизайна. И без добавления кода в step definitions тоже никак не обойтись.

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

Надеюсь, эта статья помогла вам узнать чуть больше о таком не самом распространенном, но очень интересном звере в мире мобильных UI авто-тестов как Cucumber. Если у вас есть вопросы, жду их в комментариях, постараюсь все прочитать и на все ответить.

Подробнее..

Перевод Нестабильные(Flaky) тесты одна из основных проблем автоматизированного тестирования

26.04.2021 10:24:01 | Автор: admin

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

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

Данная статья призвана рассказать как бороться с каждой из причин.

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

  • Сами тесты;

  • Фреймворк для запуска тестов;

  • Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк;

  • Операционная система и устройство с которым взаимодействует фреймворк автотестирования.

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

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

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

Сами тесты

Сами тесты могут вызвать нестабильность. Типичные причины:

  • Неправильная инциализация или очистка;

  • Неправильно подобранные тестовые данные;

  • Неправильное предположение о состоянии системы. Примером может служить системное время;

  • Зависимость от асинхроных действий;

  • Зависимость от порядка запуска тестов.

Фреймворк для запуска тестов

Ненадежный фреймворк для запуска тестов может привести к нестабильности. Типичные причины:

  • Неспособность выделить достаточно ресурсов для тестируемой системы, что приводит к ее сбою;

  • Неправильное планирование тестов, поэтому они "противоречат" и приводят к сбою друг друга;

  • Недостаточно системных ресурсов для выполнения требований тестирования.

Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк

Приложение (или тестируемая система) может быть источником нестабильности

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

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

Типичные причины:

  • Состояние гонки;

  • Непроинициализированные переменные;

  • Медленный ответ или отсутствие ответа при запросе от теста;

  • Утечки памяти;

  • Избыточная подписка на ресурсы;

  • Изменения в приложении и в тестах происходят с разной скоростью.

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

Герметичная среда менее подвержена нестабильности.

Операционная система и устройство с которым взаимодействует фреймворк автотестирования

Наконец, оборудование и операционная система могут быть источником нестабильности тестов. Типичные причины включают:

  • Сбои или нестабильность сети;

  • Дисковые ошибки;

  • Ресурсы, потребляемые другими задачами / службами, не связанными с выполняемыми тестами.

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

В следующих статьях мы рассмотрим способы решения этих проблем.

Ссылки на источники

Подробнее..

Перевод Нестабильные тесты одна из основных проблем автоматизированного тестирования(Часть 2)

05.05.2021 08:04:43 | Автор: admin

Это продолжение серии статей о нестабильных тестах.

В первой статье(оригинал/перевод на хабре) говорилось о 4 компонентах, в которых могут возникать нестабильные тесты.

В этой статье дадим советы как избежать нестабильных тестов в каждом из 4 компонентов.

Компоненты

Итак 4 компонента в которых могут возникать нестабильные тесты:

  • Сами тесты;

  • Фреймворк для запуска тестов;

  • Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк;

  • Операционная система и устройство с которым взаимодействует фреймворк автотестирования.

Это отображено на рисунке 1.

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

Сами тесты

Сами тесты могут быть нестабильными.

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

Таблица 1 Причины, варианты локализации проблемы и варианты решения нестабильности в самих тестах.

Причины нестабильных тестов

Варианты локализации проблемы

Варианты решения

Неправильная инициализация или очистка.

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

Явно инициализируйте все переменные правильными значениями перед их использованием. Правильно настройте и очистите тестовую среду. Убедитесь, что первый тест не вредит состоянию тестовой среды.

Неправильно подобранные тестовые данные.

Перезапустите тесты самостоятельно.

Сделайте тесты независимыми от какого-либо состояния из других тестов и предыдущих запусков.

Неправильное предположение о состоянии системы. Примером может служить системное время.

Проверьте зависимости приложения.

Удалите или изолируйте зависимости вашего приложение от аспектов среды, которые вы не контролируете.

Зависимости от времени выполнения, ожидание появления асинхронных событий в определенном порядке, ожидание без тайм-аутов или состояние гонки между тестами и приложением.

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

Добавьте в тесты элементы синхронизации, чтобы они ждали определенных состояний приложения. Отключите ненужное кеширование, чтобы иметь предсказуемый график ответов приложения. НЕ ДОБАВЛЯЙТЕ явные ожидания, это может привести к нестабильности тестов в будущем.

Зависимость от порядка запуска тестов (Вариант решения схож с второй причиной).

Перезапустите тесты самостоятельно.

Сделайте тесты независимыми от какого-либо состояния из других тестов и предыдущих запусков.

Фреймворк для запуска тестов

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

Таблица 2 Причины, варианта локализации проблемы, и варианты решения нестабильности в фреймворке для запуска тестов

Причины нестабильных тестов

Варианты локализации проблемы

Варианты решения

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

Проверьте логи, чтобы удостовериться появилось ли приложение.

Выделите достаточно ресурсов.

Неправильное планирование тестов, поэтому они "противоречат" и приводят к сбою друг друга.

Запустите тесты в другом порядке.

Сделайте тесты независимыми друг от друга.

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

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

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

Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк

Приложение (или тестируемая система) может быть источником нестабильности.

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

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

Таблица 3 Причины, варианта локализации проблемы, и варианты решения нестабильности в приложении или тестируемой системе

Причины нестабильных тестов

Варианты локализации проблемы

Варианты решения

Состояние гонки.

Логируйте доступ к общим ресурсам.

Добавьте в тесты элементы синхронизации, чтобы они ждали определенных состояний приложения. НЕ ДОБАВЛЯЙТЕ явные ожидания, это может привести к нестабильности тестов в будущем.

Непроинициализированные переменные.

Ищите предупреждения компилятора о неинициализированных переменных.

Явно инициализируйте все переменные правильными значениями перед их использованием.

Медленный ответ или отсутствие ответа при запросе от теста.

Логируйте время когда делаются запросы и ответы.

Проверьте и устраните все причины задержек.

Утечки памяти.

Посмотрите на потребление памяти во время прогона тестов. В обнаружении проблемы поможет инструмент Valgrind.

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

Избыточная подписка на ресурсы.

Проверьте логи, чтобы узнать не закончились ли ресурсы.

Выделите достаточно ресурсов для запуска тестов.

Изменения в приложении и в тестах происходят с разной скоростью.

Изучите историю изменений.

Введите правило при изменении кода, писать на это тесты.

Операционная система и устройство с которым взаимодействует фреймворк автотестирования

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

Таблица 4 Причины, варианта локализации проблемы, и варианты решения нестабильности в ОС и устройстве с которым взаимодействует фреймворк автотестирования

Причины нестабильных тестов

Варианты локализации проблемы

Варианты решения

Сбои или нестабильность сети.

Проверьте наличие ошибок в системных логах.

Исправьте аппаратные ошибки либо запускайте тесты на другом оборудовании.

Дисковые ошибки.

Проверьте наличие ошибок в системных логах.

Исправьте аппаратные ошибки либо запускайте тесты на другом оборудовании.

Ресурсы, потребляемые другими задачами / службами, не связанными с выполняемыми тестами.

Изучите активность системного процесса.

Сократите активность процессов не связанных с прогоном тестов.

Заключение

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

Ссылки на источники

Подробнее..

Перевод Совет инженерам по тестированию 1 Докеризируйте ваш Selenium Grid

03.09.2020 18:19:12 | Автор: admin
И снова здравствуйте. Перевели для вас полезную заметку в преддверии старта курса Java QA Engineer.





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

Selenium Grid, как известно, сложен в настройке, нестабилен и сложен в развертывании и/или управлении версиями на CI конвейере. Более простой, стабильный и удобный способ использовать предварительно созданные образы Selenium Docker.

Примечание: Единственным недостатком этого метода является то, что не поддерживается IE (Internet Explorer), так как операционную систему Windows на данный момент невозможно поместить в контейнер.


Подготовка к работе


Чтобы приступить к работе, вам необходимо сначала установить Docker и Docker Compose на вашем компьютере. Если вы используете Windows 10 или Mac, они оба будут установлены с помощью Docker Desktop.

Запуск вашего Grid


Официальный репозиторий Selenium на Docker Hub содержит предварительно созданные образы Docker для ваших нодов (узлов) Selenium Hub, Firefox и Chrome.

Самый простой способ использовать их в локальном Selenium Grid создать файл Docker Compose в корневом каталоге вашего проекта. Назовите файл просто docker-compose.yml.

Я включил пример ниже, который создает следующий Grid:
  • Один Selenium Hub
  • Один нод Chrome
  • Один нод Firefox


#docker-compose.ymlversion: "3"services:  selenium-hub:image: selenium/hub:3.141.59-neoncontainer_name: selenium-hubports:  - "4444:4444"  chrome:image: selenium/node-chrome:3.141.59-neonvolumes:  - /dev/shm:/dev/shmdepends_on:  - selenium-hubenvironment:  - HUB_HOST=selenium-hub  - HUB_PORT=4444  firefox:image: selenium/node-firefox:3.141.59-neonvolumes:  - /dev/shm:/dev/shmdepends_on:  - selenium-hubenvironment:  - HUB_HOST=selenium-hub  - HUB_PORT=4444


Файл Docker Compose описывает настройку вашего Grid. Дополнительные сведения о создании файлов Docker Compose см. в официальной документации.

Чтобы запустить свой Grid, просто используйте любой терминал (PowerShell или cmd в Windows), в котором выполните следующую команду из корневого каталога вашего проекта:

docker-compose up


Подключение к Grid


Вы можете подключиться к своему Selenium Grid точно в так же, как обычно, поскольку Hub прослушивает порт 4444 вашей локальной машины. Вот пример, в котором мы настроили наш Driver для использования нашего Chrome Node.

// Driver.javaprotected static RemoteWebDriver browser;DesiredCapabilities cap = new DesiredCapabilities();ChromeOptions chromeOptions = new ChromeOptions();            cap.setCapability(ChromeOptions.CAPABILITY, chromeOptions);            cap.setBrowserName("chrome");            driver = new RemoteWebDriver(cap);


Затем вы можете использовать библиотеку TestNG, чтобы запускать тесты на нескольких нодах параллельно, как обычно.

Стоит отметить, что на каждом ноде можно запустить несколько браузеров. Однако это не рекомендуется, и использование одного браузера на каждом ноде считается лучшей практикой для достижения оптимальной производительности.

Дополнительные советы и хитрости


Если вы хотите увидеть, что происходит в браузере, в целях отладки своих тестов, то стоит иметь debug версию вашего docker-compose.yml файла, который загружает debug браузерные ноды. Они содержат сервер VNC, поэтому вы можете наблюдать за браузером во время выполнения теста.

Также возможно запускать headlessly-браузеры для увеличения скорости (обычный способ), и Selenium также предоставляет base версии образов, так что вы можете создавать свои собственные образы, если вам нужно установить дополнительное программное обеспечение.

Чтобы создать стабильную версию Grid для вашего CI конвейера, также можно развернуть ваш Grid на Kubernetes или Swarm. Это гарантирует быстрое восстановление или замену докеров в случае их выхода из строя.



Читать ещё:



Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru