Настройка Gradle зависимостей для исключения logback-android только в тестах
Решение конфликта SLF4J провайдеров в Gradle. Как исключить logback-android только для юнит-тестов, сохранив логирование в основном Android приложении.
Как правильно настроить зависимости в Gradle, чтобы исключение logback-android применялось только для юнит-тестов, но сохранялось для основного Android приложения?
Мой проект использует Gradle для сборки, и в файле сборки есть следующие зависимости:
dependencies {
implementation libs.android.room.runtime
implementation libs.android.work.multiprocess
implementation libs.android.work.runtime
implementation libs.appcompat
implementation libs.constraintlayout
implementation libs.legacy.support.v4
implementation libs.material
implementation libs.okhttp.bom
implementation libs.slf4j
implementation project(':lib')
testImplementation libs.android.room.testing
testImplementation libs.json.org
testImplementation libs.junit
testImplementation libs.mockito
runtimeOnly libs.logback.android
testRuntimeOnly libs.logback.classic
androidTestImplementation libs.android.work.testing
androidTestImplementation libs.espresso.core
androidTestImplementation libs.ext.junit
}
При запуске юнит-тестов возникает предупреждение, за которым следует ошибка, прерывающая выполнение тестов:
SLF4J(W): Class path contains multiple SLF4J providers.
SLF4J(W): Found provider [ch.qos.logback.classic.spi.LogbackServiceProvider@560348e6]
SLF4J(W): Found provider [org.slf4j.impl.LoggerServiceProvider@1df8b5b8]
SLF4J(W): See https://www.slf4j.org/codes.html#multiple_bindings for an explanation.
Если я закомментирую строку runtimeOnly libs.logback.android, то юнит-тесты запускаются с логированием, но Android приложение теряет логирование. Добавление исключения в конфигурацию:
configurations {
all {
exclude group: "com.github.tony19", module: "logback-android"
}
}
работает аналогично закомментированию зависимости. Однако, если изменить all на test, то снова возникает ошибка дублирования провайдеров. Как настроить зависимости так, чтобы исключение применялось только для юнит-тестирования?
Проблема конфликта SLF4J провайдеров возникает из-за одновременного присутствия logback-android и logback.classic в classpath тестов. Вот правильная настройка зависимостей Gradle для исключения logback-android только для юнит-тестов:
dependencies {
// Основные зависимости приложения
implementation libs.android.room.runtime
implementation libs.android.work.multiprocess
implementation libs.android.work.runtime
implementation libs.appcompat
implementation libs.constraintlayout
implementation libs.legacy.support.v4
implementation libs.material
implementation libs.okhttp.bom
implementation libs.slf4j
implementation project(':lib')
// Тестовые зависимости
testImplementation libs.android.room.testing
testImplementation libs.json.org
testImplementation libs.junit
testImplementation libs.mockito
// Ключевые настройки для логирования
runtimeOnly libs.logback.android // Для основного приложения
testRuntimeOnly libs.logback.classic // Только для тестов
// Android тесты
androidTestImplementation libs.android.work.testing
androidTestImplementation libs.espresso.core
androidTestImplementation libs.ext.junit
}
// Решение: исключаем logback-android только для тестовых конфигураций
configurations {
testRuntimeOnly {
exclude group: "com.github.tony19", module: "logback-android"
}
}
Понимание конфигураций зависимостей Gradle для Android
Gradle использует разные конфигурации зависимостей для разных этапов сборки. Это ключевое для решения вашей проблемы. Как объясняется в документации Gradle, основные конфигурации:
| Конфигурация | Наследуется | Компиляция | Рантайм | Описание |
|---|---|---|---|---|
implementation |
нет | да | да | Доступна только в текущем модуле |
api |
нет | да | да | Экспортируется для других модулей |
runtimeOnly |
нет | нет | да | Только для выполнения приложения |
testImplementation |
нет | да | да | Для компиляции тестов |
testRuntimeOnly |
нет | нет | да | Только для выполнения тестов |
Как отмечено в статье о различиях конфигураций, правильный выбор конфигураций критически важен для оптимизации сборки и избежания конфликтов.
Проблема дублирования SLF4J провайдеров в юнит-тестах
Ошибка возникает потому, что в classpath тестов присутствуют оба провайдера SLF4J:
logback-androidизruntimeOnly(унаследован в тесты)logback.classicизtestRuntimeOnly
SLF4J не поддерживает одновременное использование нескольких провайдеров, что вызывает сбой. Как показано в обсуждении на Stack Overflow, наследование зависимостей между конфигурациями - частая причина таких конфликтов.
Правильное исключение logback-android только для тестов
Решение заключается в целевом исключении зависимости только для тестовой конфигурации. Вместо глобального исключения через configurations.all, что влияет на все сборки, нужно использовать конкретную конфигурацию:
configurations {
// Исключаем logback-android только для тестового рантайма
testRuntimeOnly {
exclude group: "com.github.tony19", module: "logback-android"
}
}
Этот подход гарантирует:
logback-androidостается в основном приложении (runtimeOnly)- Тесты используют только
logback.classicизtestRuntimeOnly - Android-тесты продолжают работать с
logback-android
Конфигурации testImplementation и testRuntimeOnly в Gradle
Важное различие между тестовыми конфигурациями, как описано в документации Gradle:
testImplementation: Зависимости, необходимые для компиляции тестов (API, заглушки)testRuntimeOnly: Зависимости, необходимые только для выполнения тестов (движки тестирования)
Для JUnit 5 это выглядит так:
// API для написания тестов
testImplementation "org.junit.jupiter:junit-jupiter-api:5.8.2"
// Движок для выполнения
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.8.2"
Практическое решение конфликта зависимостей
Для вашего проекта оптимальная конфигурация:
dependencies {
// Основные зависимости
implementation libs.slf4j
runtimeOnly libs.logback.android // Логирование в приложении
// Тестовые зависимости
testImplementation libs.junit
testRuntimeOnly libs.logback.classic // Только для тестов
// Android тесты
androidTestImplementation libs.espresso.core
androidTestImplementation libs.ext.junit
// androidTestRuntimeOnly libs.logback.android // Если нужно для UI-тестов
}
// Ключевое решение
configurations {
testRuntimeOnly {
exclude group: "com.github.tony19", module: "logback-android"
}
}
Если для Android UI-тестов (androidTestImplementation) тоже требуется логирование, добавьте:
androidTestRuntimeOnly libs.logback.android
Оптимизация сборки Android проекта с Gradle
Дополнительные рекомендации для улучшения производительности сборки:
- Изоляция зависимостей:
dependencies {
api libs.slf4j // Если lib использует SLF4J в своем API
implementation libs.logback.android
}
- Кэширование зависимостей:
gradle.properties
org.gradle.caching=true
org.gradle.parallel=true
- Версионирование зависимостей:
// В build.gradle
ext {
slf4jVersion = "2.0.7"
logbackAndroidVersion = "3.0.0"
}
dependencies {
implementation "org.slf4j:slf4j-api:${slf4jVersion}"
runtimeOnly "com.github.tony19:logback-android:${logbackAndroidVersion}"
}
Как отмечено в обсуждении Gradle Forums, правильная настройка конфигураций зависимостей критически важна для избежания конфликтов и оптимизации сборки.
Источники
- Stack Overflow на русском — Исключение пакета из зависимости в Gradle: https://ru.stackoverflow.com/questions/711622/gradle-исключить-пакет-из-зависимости
- Stack Overflow — Различие между implementation, api и compile: https://stackoverflow.com/questions/44493378/whats-the-difference-between-implementation-api-and-compile-in-gradle
- Medium — Детальное объяснение конфигураций Gradle: https://abhiappmobiledeveloper.medium.com/difference-between-implementation-api-compile-and-runtimeonly-in-gradle-dependency-55b70215d245
- Gradle Forums — Проблемы с testRuntimeOnly конфигурацией: https://discuss.gradle.org/t/resolving-dependency-configuration-testruntimeonly-is-not-allowed/40583
- Gradle Documentation — Конфигурации для тестирования: https://docs.gradle.org/current/userguide/java_testing.html
Заключение
Для решения конфликта SLF4J провайдов в Gradle:
- Используйте
configurations.testRuntimeOnly { exclude ... }для целевого исключения - Сохраняйте
runtimeOnly libs.logback.androidдля основного приложения - Добавляйте
testRuntimeOnly libs.logback.classicтолько для тестов - Для Android UI-тестов добавьте отдельную конфигурацию при необходимости
Этот подход гарантирует, что логирование будет работать и в приложении, и в тестах, но без конфликта SLF4J провайдеров. Правильная настройка зависимостей в Gradle — ключ к стабильной и быстрой сборке Android проектов.
Для исключения пакета из зависимости в Gradle используется синтаксис exclude. Вот несколько способов:
- Исключение в секции зависимостей:
dependencies {
compile('rhino:js:1.7.7') {
exclude group: 'com.github.tony19', module: 'logback-android'
}
}
- Глобальное исключение через конфигурации:
configurations {
all {
exclude group: "com.github.tony19", module: "logback-android"
}
}
- Исключение для определенной конфигурации:
configurations {
compile {
exclude group: "com.github.tony19", module: "logback-android"
}
}
Важно понимать, что exclude удаляет только транзитивные зависимости. Если пакет является прямой зависимостью, его нужно исключать через transitive = false.
Различия между Gradle конфигурациями зависимостей:
| Конфигурация | Наследуется | Компиляция | Рантайм | Описание |
|---|---|---|---|---|
| implementation | нет | да | да | Зависимость доступна только на уровне модуля, не экспортируется |
| api | нет | да | да | Зависимость доступна на уровне модуля и экспортируется для других модулей |
| compile | наследуется | да | да | Устаревшая конфигурация (заменена на implementation/api) |
| runtimeOnly | нет | нет | да | Зависимость доступна только на рантайме, не нужна для компиляции |
| testImplementation | нет | да | да | Зависимость только для тестов на уровне компиляции |
| testRuntimeOnly | нет | нет | да | Зависимость только для тестов на уровне рантайма |
Особенности testRuntimeOnly:
- Объявляет зависимости, которые требуются только во время выполнения тестов
- Не требуются во время компиляции тестов
- Используется для тестовых рантайм зависимостей, таких как движки тестирования
Разница между implementation, API, compile и runtimeOnly в Gradle:
- implementation:
- Зависимость доступна только внутри текущего модуля
- Не экспортируется для других модулей
- Изменение API такой зависимости не вызовет перекомпиляцию зависимых модулей
- Повышает скорость сборки за счет изоляции зависимостей
- api:
- Зависимость доступна внутри текущего модуля и экспортируется
- Изменение API такой зависимости вызовет перекомпиляцию всех зависимых модулей
- Используется для библиотек, которые являются частью публичного API
- compile (устаревшая):
- В Gradle 7.0 заменена на implementation и api
- Имела те же возможности, что и api
- runtimeOnly:
- Зависимость доступна только на рантайме
- Не требуется для компиляции
- Используется для зависимостей, которые нужны только при запуске приложения
Для тестовых конфигураций:
- testImplementation: зависимости для компиляции тестов
- testRuntimeOnly: зависимости для выполнения тестов
Важно правильно выбирать конфигурации для оптимизации сборки и избежания конфликтов зависимостей.
Проблема с конфигурацией testRuntimeOnly в Gradle 7:
После обновления до Gradle 7 и замены testRuntime на testRuntimeOnly в build.gradle возникает ошибка:
“Resolving dependency configuration ‘testRuntimeOnly’ is not allowed as it is defined as ‘canBeResolved=false’”
Причина:
- В Gradle 7 testRuntimeOnly настроен как ‘canBeResolved=false’
- Это означает, что конфигурация не может быть разрешена напрямую
- Вместо этого нужно использовать резолвимую конфигурацию, которая наследует testRuntimeOnly
Решение:
configurations {
testRuntimeClasspath {
extendsFrom testRuntimeOnly
}
}
Или использовать существующую конфигурацию testRuntimeClasspath вместо testRuntimeOnly.
Testing in Java & JVM projects:
Gradle автоматически создает конфигурации зависимостей для тестовых исходников:
- testImplementation: зависимости, требуемые для компиляции тестов
- testRuntimeOnly: зависимости, требуемые только во время выполнения тестов
Важно понимать, что тестовый исходный набор автоматически создает соответствующие конфигурации зависимостей, из которых наиболее полезными являются testImplementation и testRuntimeOnly, которые плагины интегрируют в путь к классам тестовой задачи.
Пример настройки тестовых зависимостей:
dependencies {
testImplementation 'junit:junit:4.12'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.2.0'
}
Эти конфигурации позволяют разделять зависимости, необходимые для компиляции тестов, и зависимости, необходимые только для их выполнения.
Пример использования testRuntimeOnly в контексте JUnit 5:
//(Required) Writing and executing Unit Tests on the JUnit Platform
testImplementation "org.junit.jupiter:junit-jupiter-api:5.2.0"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.2.0"
// (Optional) If you need "Parameterized Tests"
testImplementation "org.junit.jupiter:junit-jupiter-params:5.2.0"
// (Optional) If you also have JUnit 4-based tests
testImplementation "junit:junit:4.12"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:5.2.0"
testImplementation "io.mockk:mockk:1.8.5"
Здесь:
- testImplementation используется для API и параметризованных тестов
- testRuntimeOnly используется для движков выполнения тестов
- Это позволяет избежать включения движков в compile classpath