Skip to content

SonarQube

Calidad en el código

Esta herramienta gratuita nos permite hacer una inspección del código, prevenir bugs reconocidos y reportarnos propuestas de mejoras para nuestro código o prácticas inapropiadas, aunque es finalmente el desarrollador o alguien del equipo quién puede hacer una lectura y determinar que se considera relevante en un análisis obtenido (sobretodo ante comentarios minuciosos de la herramienta).

En otras palabras, SonarQube se enfoca en la calidad del código y suele usarse para verificar cierto estado del código en un proceso de desarrollo automatizado.

Instalacion en Docker

Aunque actualmente Sonar tiene version en la nube (SonarCloud), es posible usarlo en nuestra máquina con contenedores. A continuación se ilustran comandos para lograr un entorno local de Sonar usando Doccker (o Podman en su reemplazo).

bash
docker volume create sonarqube_data
docker volume create sonarqube_plug
docker volume create sonarqube_logs

docker run -d --name sonarqube \
-p 9000:9000 \
-v sonarqube_data:/opt/sonarqube/data \
-v sonarqube_plug:/opt/sonarqube/extensions \
-v sonarqube_logs:/opt/sonarqube/logs \
sonarqube:10.7.0-community

Esto habilita un servicio en localhost:9000 cuyo usuario y contraseña inicial es admin.
Puedes sustituir el comando docker por podman si prefieres usar Podman.

Preparando un proyecto con Java/Jacoco & SonarQube

Para probar SonarQube implementaremos un programa sencillo en Java con un código para Main como el siguiente:

java
package org.example;

public class Main {
    public static void main(String[] args) {
        String s = (args.length > 0) ? args[0] : "Welcome here!";
        System.out.println("=> ".concat(String.valueOf(numberOfWords(s))));
    }

    public static int numberOfWords(String s) {
        if (s.isEmpty()) return 0;
        String[] words = s.split("\\s+");
        return words.length;
    }
}

Y para pruebas unitarias tendremos MainTest con el siguiete código:

java
package org.example;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MainTest {
    @Test
    void main() {
        Main.main(new String[]{"This just works!"});
    }

    @Test
    void numberOfWordsEmptyString() {
        assertEquals(0, Main.numberOfWords(""));
    }

    @Test
    void numberOfWords() {
        assertEquals(2, Main.numberOfWords("Welcome here!"));
    }
}

El archivo build.gradle tendría un contenido como el siguiente:

groovy
plugins {
    id 'java'
    id 'org.sonarqube' version '5.0.0.4638'
    id 'jacoco'
}

group = 'org.example'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

sonar {
    properties {
        property "sonar.sourceEncoding", "UTF-8"
        property "sonar.sources", "src/main/java"
        property "sonar.tests", "src/test/java"
        property "sonar.test.inclusions", "**/src/test/**"
        property "sonar.java.binaries", "build/classes"
        property "sonar.java.coveragePlugin", "jacoco"
        property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/tests/test/jacocoTestReport.xml"
    }
}
test {
    useJUnitPlatform()
}

Como aspectos claves para el bloque sonar tendríamos la propiedad sonar.sources con carpeta de fuentes, sonar.test con carpeta de pruebas unitarias, sonar.java.coveragePlugin indicando el uso de jacoco, y sonar.coverage.jacoco.xmlReportPaths para reporte de cobertura.
Podría incluirse la propiedad sonar.exclusions para indicar rutas a exluir de pruebas. Por ejemplo: sonar.exclusions","**src/application/main/java/config/**

Si tenemos SonarQube configurado, habiendo generado un Token (en la opción de administración, cuyo menú se ubica en la esquina superior derecha, luego en seguridad), podríamos ejecutar desde una terminal un comando como el siguiente:

bash
./gradlew sonar -Dsonar.projectKey=java-tests -Dsonar.host.url=http://localhost:9000 -Dsonar.token=squ_d0f0e905aae9c867d5ae4686f1e63c5778444a1a

Sobre Quality Gateway

El Quality Gateway define criterios que el código debe cumplir para considerarse apto para producción:

  • Coverage: Mínimo 80% de cobertura de código
  • Duplicated Lines: Máximo 3% de líneas duplicadas
  • Maintainability Rating: Calificación A (deuda técnica < 5%)
  • Reliability Rating: Calificación A (sin bugs)
  • Security Rating: Calificación A (sin vulnerabilidades)
bash
# Verificar estado del Quality Gateway
curl -u $SONAR_TOKEN: \
  "$SONAR_HOST/api/qualitygates/project_status?projectKey=$PROJECT_KEY"

El Quality Gateway actúa como un checkpoint automático que puede bloquear deployments si el código no cumple los estándares establecidos, garantizando que solo código de calidad llegue a producción. Esta integración con pipelines CI/CD convierte a SonarQube en una herramienta esencial para mantener la calidad del software de forma continua.

Sobre SonarQube Cloud

SonarQube Cloud es la versión en la nube de SonarQube que elimina la necesidad de mantener infraestructura propia. Se integra directamente con repositorios de GitHub, GitLab, Bitbucket y Azure DevOps.

Veamos un ejemplo para GitHub Actions (archivo: .github/workflows/sonar.yml):

yaml
name: SonarCloud
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  sonarcloud:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Setup JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: 17
        distribution: 'temurin'
    - name: SonarCloud Scan
      uses: SonarSource/sonarcloud-github-action@master
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Y para Azure DevOps tendríamos un archivo azure-pipelines.yml con un contendo como el siguiente:

yaml
trigger:
- main

pool:
  vmImage: 'ubuntu-latest'

variables:
  SONAR_TOKEN: $(sonarToken)

steps:
- task: JavaToolInstaller@0
  inputs:
    versionSpec: '17'
    jdkArchitectureOption: 'x64'
    jdkSourceOption: 'PreInstalled'

- task: SonarCloudPrepare@1
  inputs:
    SonarCloud: 'SonarCloud'
    organization: 'your-org'
    scannerMode: 'Other'

- task: Gradle@2
  inputs:
    workingDirectory: ''
    gradleWrapperFile: 'gradlew'
    gradleOptions: '-Xmx3072m'
    tasks: 'test jacocoTestReport sonar'

- task: SonarCloudPublish@1
  inputs:
    pollingTimeoutSec: '300'