Quality gates: Spotless, JaCoCo, SonarCloud
Spotless — formateo determinista
google-java-format fija el estilo; removeUnusedImports + endWithNewline
dejan el diff limpio. Corre spotlessApply local y spotlessCheck en CI.
spotless { java { googleJavaFormat(property("google.java.format.version").toString()) target("src/**/*.java") trimTrailingWhitespace() endWithNewline() removeUnusedImports() }}JaCoCo — cobertura agregada multi-módulo
Cada test corre y dispara su reporte:
tasks.withType<Test> { useJUnitPlatform() finalizedBy("jacocoTestReport")}Un task agregador junta la cobertura de todos los subprojects en un solo reporte
(XML para Sonar, HTML para humanos), excluyendo config:
tasks.register<JacocoReport>("codeCoverageReport") { group = JavaBasePlugin.VERIFICATION_GROUP dependsOn(subprojects.mapNotNull { it.tasks.findByName("test")?.let { _ -> it.tasks.named("test") } })
val excluded = listOf("**/config/**") val allClassDirs = files(); val allSourceDirs = files(); val allExecData = files()
subprojects { plugins.withType<JacocoPlugin>().configureEach { tasks.withType<Test>().configureEach { extensions.findByType<JacocoTaskExtension>()?.destinationFile?.let { allExecData.from(it) } } val main = extensions.getByType<SourceSetContainer>().findByName("main") ?: return@configureEach allSourceDirs.from(main.allSource.srcDirs) allClassDirs.from(main.output.classesDirs.asFileTree.matching { exclude(excluded) }) } }
classDirectories.setFrom(allClassDirs) sourceDirectories.setFrom(allSourceDirs) executionData.setFrom(allExecData) reports { xml.required = true; html.required = true }}Tip — coverage a la vista en CI: un task que parsea los XML de JaCoCo e imprime una tabla instruction/branch/line por módulo + total. Evita abrir el HTML para ver si bajó la cobertura.
tasks.register("jacocoLogCodeCoverage") { group = JavaBasePlugin.VERIFICATION_GROUP dependsOn("codeCoverageReport") doLast { // parse cada build/reports/jacoco/test/jacocoTestReport.xml, // suma counters INSTRUCTION/BRANCH/LINE por módulo e imprime tabla + TOTAL }}------------------------------------------------------------------------------------------ JaCoCo Code Coverage Summary------------------------------------------------------------------------------------------ Module Instruction% Branch% Line%------------------------------------------------------------------------------------------ service-domain 92.10% 84.00% 93.40% service-application 88.30% 79.10% 90.02%------------------------------------------------------------------------------------------ TOTAL 90.44% 81.55% 91.71%------------------------------------------------------------------------------------------SonarCloud
Apunta al reporte JaCoCo agregado; excluye tests, config y DTOs del cálculo.
val subprojectsReportPath = subprojects.joinToString(",") { "$projectDir/${it.name}/build/reports/jacoco/test/jacocoTestReport.xml" }
sonar { properties { property("sonar.projectKey", "codehunters-example-service") property("sonar.organization", "codehunters") property("sonar.host.url", "https://sonarcloud.io") property("sonar.token", System.getenv("SONAR_TOKEN") ?: "") property("sonar.java.coveragePlugin", "jacoco") property("sonar.coverage.jacoco.xmlReportPaths", subprojectsReportPath) property("sonar.exclusions", "**/src/test/**,**/*Test.java,**/*IT.java") property("sonar.coverage.exclusions", "**/config/**") property("sonar.cpd.exclusions", "**/*Config*") }}Tip: sonar.token vía env var (SONAR_TOKEN), nunca en el repo. En CI se
inyecta como secret.