Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Хабр, привет!
Меня зовут Борис. Я Mobile QA lead в Vivid Money.
Это вступительная статья в цикле статей по iOS-автоматизации, в которых я расскажу о том, как ускорить прохождение UI-тестов.
Данная статья будет полезна iOS-автоматизаторам с опытом, либо разработчикам.
В рамках этой статьи мы разберем такие этапы:
зачем ускорять время прохождения UI-тестов;
что такое Test runner, и какие они бывают;
что нужно для прогона тестов без компиляции проекта;
делимся опытом, как это помогает нам.
Зачем ускорять время прохождения UI-тестов?
Быстрое прохождение автотестов позволяет использовать UI-тесты как можно чаще: на merge request, во время регрессионого тестирования и.т.д. Что позволяет отлавливать больше багов и меньше тратить время на поиск дефектов руками. Но задумываться об ускорении не всегда целесообразно на первых порах.
Вот ситуации, когда ускорение можно отложить:
небольшое кол-во тестов(меньше 100);
тесты прогоняются ночью или рано утром и никак не затрудняют работу другим;
время прохождения тестов со сборкой проекта занимает меньше 30 минут.
Вот ситуации, когда стоит задуматься об ускорении прохождения тестов:
Запущенные тесты образуют очереди на ci и затрудняют работу разработчикам, автоматизаторам;
UI-тесты запускается одновременно с началом регрессионого тестирования, а не заранее;
Вы хотите запускать автотесты чаще чем на ночных прогонах и во время регресса.
Что такое Test runner и какие они бывают
Test runner - это библиотека или инструмент, который выбирает сборку (или каталог с исходным кодом), который содержит тесты и набор настроек, а затем выполняет их и записывает результаты тестов.
Для запуска тестов на ci есть несколько вариантов:
Xcodebuild - нативная утилита от Apple;
Fastlane - самый популярный runner для iOS;
Marathon - раннер под iOS и Android;
Emcee - раннер от Avito.
Что нужно для прогона тестов без компиляции проекта
Для этого вам понадобится Derived data и xctestrun:
Derived data - это папка, которая находится в
~/Library/Developer/Xcode/DerivedData
по дефолту. Это место, в котором xcode хранит все виды промежуточные результатов сборки, сгенерированные индексы и так далее. Расположениеderived data
можно изменить в настройках Xcode (вкладка Locations).xctestrun - это файл, формирующийся после сборки таргета с тестами. Он содержит необходимую информацию для выполнения тестов. Для каждого тестового таргета он содержит запись с путем к тестовой машине, переменные среды и аргументы командной строки.
В качестве примера рассмотрим две реализации, используя fastlane
и xcodebuild
. Поскольку под капотом всех ранеров для iOS используется xcodebuild
.
Алгоритм действий будет следующий:
Создание сборки для тестирования
Fastlane
run_tests(
derived_data_path: "~/MyProject/derivedData",
scheme: "YourProjectUITests",
build_for_testing: true
)
Xcodebuild
$ xcodebuild -workspace <your_xcworkspace> -scheme <your_scheme> -sdk iphonesimulator -destination ‘platform=iOS Simulator,name=<your_simulator>,OS=14.0’ build-for-testing
workspace – путь к
.xcworkspace
файлу. Нужно указывать, если в проекте используются workspace;scheme – название схемы с тестами, которая будет запущена;
sdk – по умолчанию используется
iphoneos
, для использованияMacOS
илиIPadOS
нужно изменить значение;destination – параметр состоит из наборов пар ключ-значения, которые описывают, на чем запускать тесты/билд.
derviedDataPath – путь, куда сохранять
derived data
после сборки проекта. По умолчанию это значение равно тому, что установлено у вас в настройках в Xcode, но для CI лучше указать относительный путь;build-for-testing – параметр для того, чтобы собрать билд для тестирования.
Запуск тестов
Мы можем запустить тесты с помощью derived data
или .xctestrun
:
Fastlane
# Запуск тестов по Derived data
run_tests(
derived_data_path: "/Users/blysikov/MyProject/Swift-Radio-Pro-master/my_folder4",
test_without_building: true,
scheme: "SwiftRadioUITests",
device: "iPhone 8",
testplan: 'Regression'
)
# Запуск тестов по Xctestrun
run_tests(
scheme: "SwiftRadioUITests",
xctestrun: "/Users/blysikov/MyProject/Swift-Radio-Pro-master/my_folder4/Build/Products/SwiftRadioUITests_Smoke_iphonesimulator15.0-arm64.xctestrun",
)
Xcodebuild
# Запуск тестов по Xctestrun
xcodebuild -workspace "UITestExample.xcworkspace" -scheme "UITestExample" -xctestrun "build/Build/Products/UITestExample_iphoneos12.2-arm64e.xctestrun" -destination "id=9b63456a33e367d45c9aja8bj9b93223ehcf79b1" -resultBundlePath "result" test-without-building
xctestrun – путь к
.xctestrun
файлу;destination – параметр состоит из наборов пар ключ-значения, которые описывают, на чем запускать тесты/билд;
resultBundlePath – путь, куда сохранять результаты прогона;
test-without-building – параметр для того, чтобы запустить прогон тестов без сборки проекта, но используя
.xctestrun
.
Как сделали мы
Описание процесса
Каждый день у нас собираются сборки для тестирования, которые отправляются в testFlight. В этот пайплайн мы добавили последним действием шаг, который собирает нам derived data тестового таргета, архивирует её, и отправляет на билд агент.
При запуске тестов мы скачиваем с билд агента derived data
, разархивируем и на ней запускаем тесты.
Установка
Для начала нам понадобится импортировать 3 библиотеки:
net-scp - Нужен для передачи файлов на удаленный хост через SCP;
rubyzip - Нужен для архивирования файлов;
fileutils - Нужен для создания директории.
Также нам нужно будет дописать 2 вспомогательных файла:
Первый будет взаимодействовать с билд агентом.
require 'net/scp'
module ScpService
def upload_derived_data(path_to_derived_data_zip)
Net::SCP.start('your_host', 'your_login') do |scp|
scp.upload(path_to_derived_data_zip, 'path_where_to_store_on_build_agent')
end
end
def download_derived_data(destination_download_path)
Net::SCP.start('your_host', 'your_login') do |scp|
scp.download('path_where_to_store_on_build_agent', destination_download_path)
end
end
end
Второй будет архивировать нашу derived data
. Архивация нужна для того, чтобы уменьшить место, которое занимает derived data
, в крупных проектах она может весить больше 10 ГБ.
require 'zip'
class ZipFileGenerator
# Initialize with the directory to zip and the location of the output archive.
def initialize(input_dir, output_file)
@input_dir = input_dir
@output_file = output_file
end
# Zip the input directory.
def write
entries = Dir.entries(@input_dir) - %w[. ..]
::Zip::File.open(@output_file, create: true) do |zipfile|
write_entries entries, '', zipfile
end
end
private
# A helper method to make the recursion work.
def write_entries(entries, path, zipfile)
entries.each do |e|
zipfile_path = path == '' ? e : File.join(path, e)
disk_file_path = File.join(@input_dir, zipfile_path)
if File.directory? disk_file_path
recursively_deflate_directory(disk_file_path, zipfile, zipfile_path)
else
put_into_archive(disk_file_path, zipfile, zipfile_path)
end
end
end
def recursively_deflate_directory(disk_file_path, zipfile, zipfile_path)
zipfile.mkdir zipfile_path
subdir = Dir.entries(disk_file_path) - %w[. ..]
write_entries subdir, zipfile_path, zipfile
end
def put_into_archive(disk_file_path, zipfile, zipfile_path)
zipfile.add(zipfile_path, disk_file_path)
end
end
Реализация
Реализация будет состоять из 2 job:
Первая джоба формирует нужные файлы и отправляет их на билд агент. Вот как это выглядит:
# Собираем таргет с тестами для формирования derivedData и xcresults
run_tests(
derived_data_path: "~/yourProject/derivedData",
scheme: "YourProjectUITests",
build_for_testing: true
)
# Архивируем deirived data
zf = ZipFileGenerator.new('directory_to_zip', 'path_to_derived_data_zip')
zf.write
# Отправляем на билд агент
upload_derived_data('path_to_derived_data_zip')
Вторая джоба запускается во время ежедневных прогонов или во время прохождение регресса. Вот как это выглядит:
# Скачиваем derived data
download_derived_data('destination_download_path')
# Разархивируем derived data
Zip::File.open('destination_download_path') do |zip_file|
zip_file.each do |f|
f_path = File.join('destination_unzip_path', f.name)
FileUtils.mkdir_p(File.dirname(f_path))
zip_file.extract(f, f_path) unless File.exist?(f_path)
end
end
# Запускаем тесты с xctestrun
run_tests(
scheme: "yourUITests",
xctestrun: "your_deived_data_folder/yourUITests_test_plan_name_iphonesimulator14.5-arm64.xctestrun"
)
Самое важное
Время прохождения нужно ускорять, когда ваши тесты:
Создают очереди на CI;
Прогон вместе со сборкой проекта занимают более 30 минут.
Если только начинаешь работать с раннерами - используй
fastlane
. В интернете куча примеров, как организовать прогон тестов на ci в связке с ним.Для прогона тестов без компиляции проекта тебе понадобится:
derived data
и .xctestrun
.Ищешь способ сократить время прогона - воспользуйся нашим подходом в разделе: “Как сделали мы”.
Данный подход позволил нам сократить время сборки проекта при запуске тестов на регрессе с 20 минут до 3 минут. 3 минуты уходит на то, чтобы скачать нужный нам архив с derived data с билд агента и разархивировать его. Дальше мы её передаем в аргументы для прогона тестов, и тесты начинают гоняться.
Полезные статьи на эту тему:
Speed up iOS CI using Test Without Building, xctestrun and Fastlane
Building from the Command Line with Xcode FAQ
RUNNING XCODE TESTS FROM CI
Интересуешься автоматизацией на iOS? Подписывайся на мой телеграмм-канал, в котором я публикую материалы, которые будут полезны как начинающим, так и опытным iOS-автоматизаторам.