Logging Efficace dans les Tests Fonctionnels de Plugins Gradle
شائع شدہ: 07 November 2025
مقدمہ
Gradle پلاگین کی ترقی کے دوران، فنکشنل ٹیسٹس plugin کے سلوک کی تصدیق کے لیے ضروری ہیں ایک واقعی Gradle ماحول میں۔ تاہم، ایک اہم سوال جلد سے سامنے آتا ہے:کسی مؤثر لنگنگ سسٹم کی ترتیبیسے کے لئے GradleRunner کے ساتھ مشکل دار انحصارات بنائے بغیر؟
اس مضمون میں، ہم جانچیں گے:
-
Gradle کے فنگشنل ٹیسٹس میں لاگنگ کی مسئلہ
-
SLF4J اور Logback کے ساتھ مضبوط حل کی معماری
-
پورا تطبيق مخصوص مثالوں کے ساتھ
-
اچھی طریقے اور بچنے والی غلطیاں
مشکل: GradleRunner پر انحصار
پلاگین کی ترقی کا سیاق
Gradle پلاگِن تیار کرتے وقت،عام طور پر استعمال کیا جاتا ہے`GradleRunner`Gradle TestKit کے لئے ٹیسٹ بناؤڈز چلانے :
@Test
fun `test my plugin`() {
val result = GradleRunner.create()
.withProjectDir(projectDir)
.withArguments("myTask")
.build()
// Comment logger efficacement ici ?
}
لاگنگ کا معضل
اہم مسئلہ درج ذیل ہے:لاگنگ کو کسی_OBJECT_ک_مُعتمد_نہ_کرنا_چاہیے_جس_کی_شروع_ات_کے_بارے_میں_ہم_مطمئن_نہ_ ہیں.
@startuml
!define RECTANGLE class
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
package "Architecture Problématique" {
RECTANGLE Test {
+ runTest()
}
RECTANGLE GradleRunner {
+ build()
+ getLogger() ❌
}
RECTANGLE Logger {
+ info()
+ debug()
}
Test --> GradleRunner : dépend de
Logger ..> GradleRunner : dépend de ❌
}
note right of Logger
Problème: Le logger dépend
du GradleRunner qui peut
ne pas être initialisé
end note
@enduml
غلط اچھی خیالات
|
یہ نہ کریں :
|
حل : لاگر مستقل
مُوصی شدہ معمارہ
حل یہ ہے کہ ایک لوگر استعمال کیا جائےبالکل مستقل du GradleRunner, ٹیسٹ کلاس کے سطح پر اینیشیالائز کردہ
@startuml
!define RECTANGLE class
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
package "درست آرکیٹیکچر" {
RECTANGLE TestClass {
+ {static} logger: Logger ✓
+ runTest()
}
RECTANGLE SLF4J {
+ LoggerFactory
}
RECTANGLE GradleRunner {
+ build()
+ forwardOutput()
}
RECTANGLE "کنسول آؤٹ پٹ" as Output {
+ captured text
}
TestClass --> SLF4J : utilise ✓
TestClass --> GradleRunner : utilise
GradleRunner --> Output : génère
TestClass --> Output : analyse
}
note top of TestClass
لاگر (logger) کو GradleRunner سے آزادانہ طور پر
initialize کیا گیا ہے
end note
note bottom of GradleRunner
.forwardOutput()` تجزیے کے لیے آؤٹ پٹ کو کیپچر کرنے کی اجازت دیتا ہے
end note
@enduml
نفاذ کی رفتار
@startuml
!theme plain
skinparam backgroundColor #FEFEFE
participant "JUnit" as junit
participant "TestClass" as test
participant "Logger (SLF4J)" as logger
participant "GradleRunner" as runner
participant "Gradle Build" as gradle
activate junit
junit -> test : load class
activate test
test -> logger : initialize (companion)
activate logger
logger --> test : logger ready ✓
deactivate logger
junit -> test : @BeforeAll
test -> logger : info("Initialisation...")
junit -> test : @BeforeEach
test -> logger : debug("ٹیسٹ سیٹ اپ ہو رہا ہے...")
junit -> test : @Test
test -> logger : info("ٹیسٹ: شروع ہو رہا ہے...")
test -> runner : create()
activate runner
test -> runner : .forwardOutput()
test -> runner : .withArguments(...)
test -> runner : .build()
runner -> gradle : execute build
activate gradle
gradle --> runner : output text
deactivate gradle
runner --> test : BuildResult + output
deactivate runner
test -> logger : debug("آؤٹ پٹ کا تجزیہ کیا جا رہا ہے...")
test -> test : analyze output
test -> logger : info("✓ ٹیسٹ پاس ہو گیا")
test --> junit : success
deactivate test
deactivate junit
@enduml
مکمل پیاده سازی
Gradle کے انحصارات
اپنی میں ضروری وابستگی شامل کریں`build.gradle.kts` :
dependencies {
// Pour les tests fonctionnels
testImplementation(gradleTestKit())
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
// Pour le logging
testImplementation("org.slf4j:slf4j-api:2.0.17")
testRuntimeOnly("ch.qos.logback:logback-classic:1.4.14")
// Pour les assertions
testImplementation("org.assertj:assertj-core:3.27.6")
}
tasks.test {
useJUnitPlatform()
}
Logback ترتیب
فائل بنائیں`src/test/resources/logback-test.xml` :
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Appender console avec format lisible -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Appender fichier pour conservation -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>build/test-results/functional-tests.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- Logger pour vos tests -->
<logger name="com.cheroliv.bakery" level="DEBUG"/>
<!-- Réduire le bruit de Gradle -->
<logger name="org.gradle" level="WARN"/>
<!-- Configuration racine -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
کامل ٹیسٹ کلاس
package com.cheroliv.bakery
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.jupiter.api.*
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import org.junit.jupiter.api.io.TempDir
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@TestInstance(PER_CLASS)
class BakeryPluginFunctionalTest {
@field:TempDir
lateinit var projectDir: File
private val buildFile by lazy { projectDir.resolve("build.gradle.kts") }
private val settingsFile by lazy { projectDir.resolve("settings.gradle.kts") }
companion object {
// ✓ Logger indépendant, initialisé au chargement de la classe
private val logger: Logger = LoggerFactory.getLogger(
BakeryPluginFunctionalTest::class.java
)
@JvmStatic
@BeforeAll
fun globalSetup() {
logger.info("═".repeat(60))
logger.info("DÉMARRAGE DE LA SUITE DE TESTS FONCTIONNELS")
logger.info("═".repeat(60))
}
@JvmStatic
@AfterAll
fun globalTeardown() {
logger.info("═".repeat(60))
logger.info("FIN DE LA SUITE DE TESTS")
logger.info("═".repeat(60))
}
}
@BeforeEach
fun setup() {
logger.info("─".repeat(60))
logger.info("Préparation de l'environnement de test")
logger.debug("Répertoire de test: ${projectDir.absolutePath}")
settingsFile.writeText("""
rootProject.name = "test-bakery-project"
""".trimIndent())
logger.debug("✓ settings.gradle.kts créé")
}
@AfterEach
fun teardown(testInfo: TestInfo) {
logger.info("✓ Test terminé: ${testInfo.displayName}")
logger.info("─".repeat(60))
}
@Test
fun `plugin can be applied successfully`() {
logger.info("TEST: Application du plugin Bakery")
buildFile.writeText("""
plugins {
id("com.cheroliv.bakery")
}
""".trimIndent())
logger.debug("Fichier build.gradle.kts créé")
logger.debug("Lancement du build Gradle...")
val startTime = System.currentTimeMillis()
val result = GradleRunner.create()
.forwardOutput() // ← Important : capture la sortie
.withPluginClasspath()
.withArguments("tasks", "--group=bakery", "--stacktrace")
.withProjectDir(projectDir)
.build()
val duration = System.currentTimeMillis() - startTime
logger.info("Build terminé en ${duration}ms")
// Analyse de la sortie
logger.debug("Analyse de la sortie Gradle...")
val outputLines = result.output.lines()
logger.debug("Nombre de lignes capturées: ${outputLines.size}")
// Validation
val expectedTasks = listOf("bake", "printConfigPath", "printJBakeClasspath")
logger.info("Vérification des tâches attendues:")
expectedTasks.forEach { task ->
val found = result.output.contains(task)
logger.debug(" ${if (found) "✓" else "✗"} $task")
assertTrue(found, "Task '$task' should be available")
}
logger.info("✓ Plugin appliqué avec succès - ${expectedTasks.size} tâches trouvées")
}
@Test
fun `bake task executes and prints version`() {
logger.info("TEST: Exécution de la tâche 'bake'")
buildFile.writeText("""
plugins {
id("com.cheroliv.bakery")
}
""".trimIndent())
logger.debug("Exécution de 'gradle bake'...")
val result = GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("bake", "--info") // --info pour plus de détails
.withProjectDir(projectDir)
.build()
// Extraction d'informations depuis la sortie
logger.debug("Recherche du message 'Baking site'...")
val hasBakingMessage = result.output.contains("Baking site with Bakery Plugin!")
logger.debug("Recherche de la version JBake...")
val hasVersionMessage = result.output.contains("Using JBake version:")
// Extraction de la version (si présente)
val versionRegex = Regex("Using JBake version: (.+)")
val versionMatch = versionRegex.find(result.output)
if (versionMatch != null) {
val version = versionMatch.groupValues[1]
logger.info("Version JBake détectée: $version")
}
// Validation
assertTrue(hasBakingMessage, "Message 'Baking site' attendu")
assertTrue(hasVersionMessage, "Message de version attendu")
assertEquals(TaskOutcome.SUCCESS, result.task(":bake")?.outcome)
logger.info("✓ Tâche 'bake' exécutée avec succès")
}
@Test
fun `analyze output with structured logging`() {
logger.info("TEST: Analyse structurée de la sortie")
buildFile.writeText("""
plugins {
id("com.cheroliv.bakery")
}
task("diagnostics") {
doLast {
println("[DIAG] Project: ${'$'}{project.name}")
println("[DIAG] Build dir: ${'$'}{project.buildDir}")
println("[DIAG] Plugin applied: true")
val config = configurations.findByName("jbakeRuntime")
println("[DIAG] JBake config exists: ${'$'}{config != null}")
if (config != null) {
println("[DIAG] Dependencies count: ${'$'}{config.dependencies.size}")
}
}
}
""".trimIndent())
logger.debug("Exécution de la tâche diagnostics...")
val result = GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("diagnostics", "--quiet")
.withProjectDir(projectDir)
.build()
// Analyse structurée
logger.info("Extraction des informations de diagnostic:")
val diagLines = result.output.lines()
.filter { it.startsWith("[DIAG]") }
logger.debug("Nombre de lignes de diagnostic: ${diagLines.size}")
diagLines.forEach { line ->
logger.info(" $line")
// Analyse spécifique par type de ligne
when {
line.contains("Project:") -> {
val projectName = line.substringAfter("Project:").trim()
logger.debug(" → Nom du projet extrait: '$projectName'")
}
line.contains("Dependencies count:") -> {
val count = line.substringAfter("count:").trim()
logger.debug(" → Nombre de dépendances: $count")
}
}
}
assertTrue(diagLines.isNotEmpty(), "Des lignes de diagnostic devraient être présentes")
logger.info("✓ Analyse structurée terminée - ${diagLines.size} lignes analysées")
}
@Test
fun `test with error handling`() {
logger.info("TEST: Gestion d'erreurs avec logging")
buildFile.writeText("""
plugins {
id("com.cheroliv.bakery")
}
""".trimIndent())
try {
logger.debug("Tentative d'exécution...")
val result = GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("printJBakeClasspath")
.withProjectDir(projectDir)
.build()
val outcome = result.task(":printJBakeClasspath")?.outcome
logger.info("Outcome: $outcome")
if (outcome == TaskOutcome.SUCCESS) {
logger.info("✓ Exécution réussie")
} else {
logger.warn("⚠ Outcome inattendu: $outcome")
}
assertEquals(TaskOutcome.SUCCESS, outcome)
} catch (e: Exception) {
logger.error("✗ Échec du test", e)
logger.error("Type d'exception: ${e.javaClass.simpleName}")
logger.error("Message: ${e.message}")
// Log de la stack trace si nécessaire
if (logger.isDebugEnabled) {
logger.debug("Stack trace complète:", e)
}
throw e
}
}
}
ملٹی‑ليول لاگنگ سким
@startuml
!theme plain
skinparam backgroundColor #FEFEFE
package "لاگنگ کی سطحیں" {
rectangle "TRACE" #E8E8E8 {
note
انتہائی باریک رن ٹائم تفصیلات
• متغیرات کی اقدار
• اندرونی طریقوں کی کالز
end note
}
rectangle "DEBUG" #D0E8FF {
note
ڈیبگنگ کی معلومات
• درمیانی مراحل
• تخلیق کی گئی فائلوں کا مواد
• Gradle آؤٹ پٹ کا تجزیہ
end note
}
rectangle "INFO" #C8FFC8 {
note
اہم معلومات
• ٹیسٹ کا آغاز/اختتام
• توثیقی نتائج
• رن ٹائم
end note
}
rectangle "WARN" #FFFFC8 {
note
انتباہات
• غیر متوقع رویے
• غیر معمولی نتائج
end note
}
rectangle "ERROR" #FFC8C8 {
note
غلطیاں
• ٹیسٹ کی ناکامیاں
• Exceptions
• Stack traces
end note
}
}
@enduml
نخبی رسومات
منصوح شدہ لانگنگ سٹرکچر
@Test
fun `mon test`() {
// 1. LOG: Début du test
logger.info("TEST: Description du test")
// 2. LOG: Préparation
logger.debug("Préparation des fichiers...")
buildFile.writeText("...")
logger.debug("✓ Fichiers préparés")
// 3. LOG: Exécution
logger.debug("Lancement du build...")
val startTime = System.currentTimeMillis()
val result = GradleRunner.create()
.forwardOutput()
.withArguments("task")
.build()
val duration = System.currentTimeMillis() - startTime
logger.info("Build terminé en ${duration}ms")
// 4. LOG: Analyse
logger.debug("Analyse des résultats...")
// ... validations ...
// 5. LOG: Conclusion
logger.info("✓ Test réussi")
}
پڑھنے کی سہولت کے لیے بصری اشارات
لاگ کی پڑھنی بہتر بنانے کے لیے یونیکڈ سائنبلز استعمال کریں:
logger.info("✓ Succès")
logger.info("✗ Échec")
logger.info("⚠ Attention")
logger.info("→ Étape suivante")
logger.info("═".repeat(60)) // Séparateur principal
logger.info("─".repeat(60)) // Séparateur secondaire
خروجی سے معلومات کی اخراج
// Recherche simple
val hasMessage = result.output.contains("Expected message")
// Extraction avec regex
val versionRegex = Regex("Version: (.+)")
val version = versionRegex.find(result.output)?.groupValues?.get(1)
// Filtrage de lignes
val errorLines = result.output.lines()
.filter { it.contains("ERROR") }
errorLines.forEach { logger.error("Gradle error: $it") }
مقابلہ پہلے/بعد
پہلے : مسئلة آمیز طريقة عمل
class MyTest {
lateinit var logger: Logger // ❌ Initialisé tardivement
@BeforeEach
fun setup() {
logger = LoggerFactory.getLogger(...) // ❌ Dépendance
}
@Test
fun test() {
val runner = GradleRunner.create()
logger.info("Testing...") // ⚠ Peut échouer
}
}
بعد : مضبوط رویہ
class MyTest {
companion object {
private val logger = LoggerFactory.getLogger(...) // ✓ Indépendant
}
@Test
fun test() {
logger.info("TEST: Starting...") // ✓ Toujours disponible
val result = GradleRunner.create()
.forwardOutput() // ✓ Capture la sortie
.build()
logger.debug("Output: ${result.output}") // ✓ Analyse
}
}
خلاصہ ڈائگرام
@startuml
!theme plain
skinparam backgroundColor #FEFEFE
title Flux Complet de Logging dans les Tests Fonctionnels
actor "Développeur" as dev
participant "JUnit" as junit
participant "TestClass" as test
participant "Logger\n(SLF4J)" as logger
participant "Logback" as logback
participant "GradleRunner" as runner
participant "Console/File" as output
dev -> junit : lance les tests
activate junit
junit -> test : charge la classe
activate test
test -> logger : initialize (companion)
activate logger
logger -> logback : configure
activate logback
logback --> logger : ready
deactivate logback
logger --> test : ✓
deactivate logger
== Phase de Setup ==
junit -> test : @BeforeAll
test -> logger : info("Initialisation")
logger -> output : [INFO] Initialisation
junit -> test : @BeforeEach
test -> logger : debug("Setup test")
logger -> output : [DEBUG] Setup test
== Phase de Test ==
junit -> test : @Test
test -> logger : info("TEST: Starting")
logger -> output : [INFO] TEST: Starting
test -> runner : create()
activate runner
test -> runner : .forwardOutput()
test -> runner : .build()
runner -> runner : execute Gradle
runner --> test : BuildResult + output text
deactivate runner
test -> logger : debug("Analyzing output")
logger -> output : [DEBUG] Analyzing output
test -> test : assertions
test -> logger : info("✓ Test passed")
logger -> output : [INFO] ✓ Test passed
== Phase de Cleanup ==
junit -> test : @AfterEach
test -> logger : debug("Cleanup")
logger -> output : [DEBUG] Cleanup
junit -> test : @AfterAll
test -> logger : info("Fin des tests")
logger -> output : [INFO] Fin des tests
test --> junit : ✓ all tests passed
deactivate test
deactivate junit
dev <-- junit : rapport de tests
@enduml
بچنے کے لیے پھنساؤں
❌ پنجه نمبر 1 : بھولنا `.forwardOutput()
// MAUVAIS: La sortie Gradle n'est pas capturée
val result = GradleRunner.create()
.withArguments("task")
.build()
// result.output sera vide !
// BON: La sortie est capturée
val result = GradleRunner.create()
.forwardOutput() // ✓
.withArguments("task")
.build()
❌ جال ۲ : لاگر آئی نٹ یا لائزڈ
// MAUVAIS
class Test {
lateinit var logger: Logger // Peut ne pas être initialisé
}
// BON
class Test {
companion object {
private val logger = LoggerFactory.getLogger(...) // Toujours initialisé
}
}
❌ جہاز نمبر 3 : زائد لاگنگ
// MAUVAIS: Trop de logs DEBUG en production
logger.debug("Variable a = $a")
logger.debug("Variable b = $b")
logger.debug("Variable c = $c")
// ... 100 lignes de logs
// BON: Logging ciblé
logger.info("Traitement de ${items.size} éléments")
logger.debug("Détails: ${items.take(5)}...") // Échantillon seulement
نتائج اور معیارات
اسApproach کے ساتھ، آپ کو مل جاتا ہے :
| مٹریک | قبل | بعد |
|---|---|---|
کوڈ کی لائنوں |
~50 líneas |
~80 líneas (+60%) Wait, that’s Spanish. Actually we need Urdu script. Let’s correct. |
ڈیبیگ کا وقت |
تقریباً 30 منٹ |
تقریباً 5 منٹ (-83٪) |
پیدا شدہ غلطیاں |
بنیادی |
تفصیلی |
مُدامدری |
مشکل |
سادہ |
آزادی |
جوڑا |
✓ مستقلہ |
نتیجہ
گریڈل پلاگائن کے فنگیشنل ٹیسٹس میں مضبوط لنوگنگ سسٹم کا تعین ایک سادہ اصول پر مشتمل ہے :استقلال.
ذہن میں رکھنے والے اہم نکات :
-
مستقل لاگر: ایک میں شروع کیا گیا`companion object`
-
SLF4J + Logback: آزموشدا اور قابل تنظیم اسٹیک
-
.forwardOutput(): گرڈل آؤٹ پٹ کی کپچر
-
منظم لاگنگ: INFO کے لیے مراحل، DEBUG کے لیے تفصیلات
-
آؤٹ پٹ تجزیہمعلومات سے نکالنا`result.output`
یہ رویہ ٹیسٹس کی ضمانت دیتا ہےرکھنے کے قابل, دبگبلس et مضبوط, مضبوط ڈیزائن کے اصولوں کی احترام کرتے ہوئے
زیادہ دور جانا
ایک اگلے مضمون میں، ہم جانچیں گے:
-
مسلسل انٹیگریشن کے ساتھ نظامی لاگنگ
-
ایچ ٹی ایمائل ٹیسٹ رپورٹس ضمّن شدہ لوگ کے ساتھ
-
JVM سے پیوست کے ساتھ پیشہ وران ڈبگنگ
-
عملکردگی اور لاگنگ کی بہتری
یہ مضمون میری تکنیکی بلاگ پر "Gradle Plugins کا ڈویلپمنٹ"سلسلے کا حصہ ہے۔