Одна из самых трудоемких операций на CI-сервере — запуск автотестов.
Есть много способов их ускорить, например, распараллеливание выполнения на нескольких CI-агентах и/или эмуляторах, полная эмуляция внешней среды (бэкенд/сервисы Google/веб-сокеты), тонкая настройка эмуляторов (отключение анимации/вебсокетов).
Безголовые сборки /отключить снимки) и так далее.
Сегодня мы поговорим об анализе воздействия или запуске только тех тестов, которые связаны с последними изменениями в коде.
Расскажу, какие шаги необходимы для анализа воздействия и как мы реализовали это в нашем проекте.
Шаг первый: получите разницу изменений.
Легче всего добиться с помощью встроенных инструментов.
Гит .
Мы обернули работу по анализу воздействия в плагине 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. Собираем дерево зависимостей вида тесткейсы -> скрипты -> описания страниц( Объект страницы ) -> узлы зависимостей со второго шага, затем идя назад получаем список всех необходимых тестов.
Дерево зависимостей 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 #Тестирование мобильных приложений #автоматизация тестирования #анализ воздействия
-
Пережить Критические Моменты
19 Oct, 24 -
Для Тех, Кто Страдает От Жары
19 Oct, 24 -
Ssd Для Серьезных Людей
19 Oct, 24