Хабр, привет!

Меня зовут Борис. Я 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:

  1. Derived data - это папка, которая находится в ~/Library/Developer/Xcode/DerivedData по дефолту. Это место, в котором xcode хранит все виды промежуточные результатов сборки, сгенерированные индексы и так далее. Расположение derived data можно изменить в настройках Xcode (вкладка Locations).

  2. 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"
)

Самое важное

  1. Время прохождения нужно ускорять, когда ваши тесты:

    1. Создают очереди на CI;

    2. Прогон вместе со сборкой проекта занимают более 30 минут.

  2. Если только начинаешь работать с раннерами - используй fastlane. В интернете куча примеров, как организовать прогон тестов на ci в связке с ним.

  3. Для прогона тестов без компиляции проекта тебе понадобится: derived data и .xctestrun.

  4. Ищешь способ сократить время прогона - воспользуйся нашим подходом в разделе: “Как сделали мы”.

Данный подход позволил нам сократить время сборки проекта при запуске тестов на регрессе с 20 минут до 3 минут. 3 минуты уходит на то, чтобы скачать нужный нам архив с derived data с билд агента и разархивировать его. Дальше мы её передаем в аргументы для прогона тестов, и тесты начинают гоняться.

Полезные статьи на эту тему:


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

Комментарии (0)