Анализ Воздействия На Примере Android-Проекта

Одна из самых трудоемких операций на CI-сервере — запуск автотестов.

Есть много способов их ускорить, например, распараллеливание выполнения на нескольких CI-агентах и/или эмуляторах, полная эмуляция внешней среды (бэкенд/сервисы Google/веб-сокеты), тонкая настройка эмуляторов (отключение анимации/вебсокетов).

Безголовые сборки /отключить снимки) и так далее.

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

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



Анализ воздействия на примере Android-проекта



Шаг первый: получите разницу изменений.

Легче всего добиться с помощью встроенных инструментов.

Гит .

Мы обернули работу по анализу воздействия в плагине Gradle и использовали Java-оболочку над Git — JGit .

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

  
  
  
  
  
  
   

val objectReader = git.repository.newObjectReader() val oldTreeIterator = CanonicalTreeParser() val oldTree = git.repository.resolve("HEAD^^{tree}") oldTreeIterator.reset(objectReader, oldTree) val newTreeIterator = CanonicalTreeParser() val newTree = git.repository.resolve("HEAD^{tree}") newTreeIterator.reset(objectReader, newTree) val formatter = DiffFormatter(DisabledOutputStream.INSTANCE) formatter.setRepository(git.repository) val diffEntries = formatter.scan(oldTree, newTree) val files = HashSet<File>() diffEntries.forEach { diff -> files.add(git.repository.directory.parentFile.resolve(diff.oldPath)) files.add(git.repository.directory.parentFile.resolve(diff.newPath)) } return files

Но ничто не мешает вам собрать все коммиты между двумя ветками:

val oldTree = treeParser(git.repository, previousBranchRef) val newTree = treeParser(git.repository, branchRef) val diffEntries = git.diff().

setOldTree(oldTree).

setNewTree(newTree).

call() val files = HashSet<File>() diffEntries.forEach { diff -> files.add(git.repository.directory.parentFile.resolve(diff.oldPath)) files.add(git.repository.directory.parentFile.resolve(diff.newPath)) } return files



private fun treeParser(repository: Repository, ref: String): AbstractTreeIterator { val head = repository.exactRef(ref) RevWalk(repository).

use { walk -> val commit = walk.parseCommit(head.objectId) val tree = walk.parseTree(commit.tree.id) val treeParser = CanonicalTreeParser() repository.newObjectReader().

use { reader -> treeParser.reset(reader, tree.id) } walk.dispose() return treeParser } }



Шаг второй: соберите дерево зависимостей исходного кода.

Детализация дерева зависит от количества кода и автотестов.

Чем больше детализация, тем выше точность выделения только необходимых тестов, но сборка дерева происходит медленнее.

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

Список модулей в проекте:

private fun findModules(projectRootDirectory: File): List<Module> { val modules = ArrayList<Module>() projectRootDirectory.traverse { file -> if (file.list()?.

contains("build.gradle") == true) { val name = file.path .

removePrefix(projectRootDirectory.absolutePath) .

replace("/", ":") val pathToBuildGradle = "${file.path}/build.gradle" val manifestFile = File("${file.path}/$ANDROID_MANIFEST_PATH") if (manifestFile.exists()) { if (modulePackage != null) { modules.add(Module(name)) } } } } return modules }

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

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



Шаг третий: выберите все затронутые узлы дерева зависимостей.

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



private fun findAllDependentModules(origin: Module, links: Set<Link>): Set<Module> { val queue = LinkedList<Module>() val visited = HashSet<Module>() queue.add(origin) val result = HashSet<Module>() while (queue.isNotEmpty()) { val module = queue.poll() if (visited.contains(module)) { continue } visited.add(module) result.add(module) queue.addAll(links.filter { it.to == module }.

map { it.from }) } return result }



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

На этом этапе нам нужно как-то связать автотесты с узлами дерева зависимостей из второго шага.

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

Мы используем фреймворк Каспрессо , а для связи тестов с деревом зависимостей мы разбираем тесты с помощью самого компилятора Kotlin. Собираем дерево зависимостей вида тесткейсы -> скрипты -> описания страниц( Объект страницы ) -> узлы зависимостей со второго шага, затем идя назад получаем список всех необходимых тестов.



Анализ воздействия на примере Android-проекта

Дерево зависимостей

implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.10")



private fun readUiTestsMetaData(modules: List<Module>): List<UiTestMetaData> { val testRootDirectory = rootDirectory.get().

resolve(TEST_ROOT_PATH) val ktFiles = kotlinFiles(testRootDirectory) val pageObjects = ktFiles.mapNotNull { parsePageObjectMetaData(it, modules) } .

sortedBy { it.name } val scenarioObjects = ktFiles.map { parseScenarioObjects(it, pageObjects) }.

flatten() val scenarios = buildScenarioMetaData(scenarioObjects, pageObjects) return ktFiles.map { parseUiTestMetaData(it, scenarios, pageObjects) } .

flatten() .

sortedBy { it.name } }



Шаг пятый: проведите необходимые тесты.

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

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

В Teamcity на этапе анализа воздействия наш плагин Gradle собирает все автотесты с четвертого шага, извлекает из них идентификатор теста и записывает его в файл.

После этого при подготовке Марафона мы скармливаем ему все эти идентификаторы и получаем запуск только нужных тестов из всех существующих.

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

Надеюсь, статья была вам полезна, и следите за обновлениями, ребята :) Теги: #Android #Разработка Android #Тестирование мобильных приложений #автоматизация тестирования #анализ воздействия

Вместе с данным постом часто просматривают: