Aller au contenu

TP progressifs et testés pour un mini-projet Spring Boot

Depuis start.spring.io

alt text

Tout tourne sur les shared runners (Linux) de GitLab. On va uiliser Maven pour les démonstrations et TP.

Arborescence de départ de notre projet minimaliste !

.
├─ src
│  ├─ main/java/fr/exemple/gitlab/tp1/Tp1Application.java
│  └─ main/java/fr/exemple/gitlab/tp1/controller/CoucouController.java
└─ src/test/java/fr/exemple/gitlab/tp1/controller/CoucouControllerTest.java

Code de base (commun Maven/Gradle)

main/java/fr/exemple/gitlab/tp1/Tp1Application.java :

package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}

main/java/fr/exemple/gitlab/tp1/controller/CoucouController.java :

package fr.exemple.gitlab.tp1.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CoucouController {
	
	@GetMapping("/coucou")
	public String coucou()
	{
		return "Coucou de Spring Boot";
	}

}

src/test/java/fr/exemple/gitlab/tp1/controller/CoucouControllerTest.java :

package fr.exemple.gitlab.tp1.controller;

import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.Test;

public class CoucouControllerTest {
	
	@Test
	void retourneCoucou() {
		var controller = new CoucouController();
		assertThat(controller.coucou()).isEqualTo("Coucou de Spring Boot");
	}

}

TP1 — Projet minimal + build

Avec Maven (pom.xml) :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.7</version>
		<relativePath/>
	</parent>
	<groupId>fr.exemple.gitlab</groupId>
	<artifactId>tp1</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<name>tp1</name>
	<description>Demo project for Spring Boot et GitLab</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

.gitlab-ci.yml (Maven)

# Pour commencer, uniquement avec un stage "build"
stages:
  - build

# ici, je nomme mon job "maven-build" je pourrai l'appeler Toto, ça marcherait aussi
maven-build:
  stage: build
  image: maven:3.9-eclipse-temurin-17 ## je choisi maven 3.9 et Eclipse Témurin avec Java 17 depuis une image de DockerHub (docker pull maven:3.9-eclipse-temurin-17)
  variables:
    MAVEN_OPTS: "-Dmaven.test.skip=false" # Cette variable permet d'exécuter les tests unitaires prévues (maven_opts est une vairiable d'environnement Maven) on a le skip à false
  script:
    - mvn -B -ntp clean package # Nettoie, compile, exécute les tests, et valide le projet proprement dans un environnement CI silencieux.
    - ls -la target/
  artifacts:
    paths: [ target/ ]
    expire_in: 1 week

## -B : c'est le mode Batch non intéractif indispensable ici avec gitlab ci
## -ntp : sert à supprimer la bannière maven 'no transfer progress'
## -q : mode dit quiet pour moins verbeux (enlever pour cette fois)
## clean : supprime le dossier target/ avant de compiler
## verify : fait le job, compile, teste et vérifie la validité du projet (finalement j'ai remplacé par package pour avoir un JAR)

## artifacts : le fichier conservé pendant 7 jours donc dans target/ on aura le .jar, les tests les rapports si besoin.

Sur GitLab :

alt text

alt text

Résultat que vous devez obtenir :

alt text

alt text

Version en enlevant le -q pour que ce soit plus verbeux (maven) et remplacement de verify par package pour être certain de générer notre application au format JAR dans le répertoire target :

alt text

alt text

Logiquement, vous devriez trouver dans artefacts un dossier nommé artefacts.zip qui contient le Jar exécutable que vous pouvez tester sur votre machine !

alt text

alt text

Pour terminer, voici comment lancer votre application sur votre machine et tester dans votre navigateur :

Lancez la commande java pour exécuter votre JAR : java -jar tp1-0.0.1-SNAPSHOT.jar

alt text

Sur votre navigateur à l’adresse http://localhost:8080/coucou

alt text

On a bien notre message que le contrôleur nous retourne !

TP2 — Linting/formating avec Checkstyle et Spotless

Nous allons faire un peu de clean code… donc pour ce tp, faites du code Java moche, je pense que vous savez le faire ;))

Vous pouvez reprendre le TP1 ou en prendre un autre.

Pour vous permettre de lancer une analyse statique, voire même, de corriger votre code java, voici 2 API bien pratiques que nous allons utiliser dans ce tp2.

Cela vous aidera à générer du code propre et cohérent dans vos projets futures !

Outil Ce qu’il fait Ce qu’il ne fait pas
Checkstyle Analyse statique (bonne pratique, lisibilité, conventions) Ne reformate pas le code
Spotless Reformate le code selon une convention (Google, Eclipse, etc.) Ne vérifie pas les bonnes pratiques

Checkstyle lit votre code sans l’exécuter pour vérifier qu’il respecte un ensemble de règles de style et de bonnes pratiques comme celle-ci :

Exemple concret (imaginons que ce soit votre code):

public class test {
public static void main(String[] args){
System.out.println("coucou");
}
}

Et bien Checkstyle vous listera vos erreurs du genre :

[ERROR] Line 1: Class 'test' should be in PascalCase.
[ERROR] Line 2: 'method def' child has incorrect indentation level 0, expected level should be 4.
[ERROR] Line 3: 'System.out' should not be used.

Voyons ce que fait son ami Spotless :

C’est simple, il fait le ménage automatique… Il corrige automatiquement le style du code (indentation, imports, formatage,…).

si vous utilisez Maven, vous pouvez les exécuter avec les commandes ci-dessous que nous allons utiliser dans notre fichier gitlab-ci.yaml

mvn checkstyle:check  # pour l'analyse du code
mvn spotless:check    # la vérification
mvn spotless:apply    # la correction auto

alt text

Bref, vous devez ajouter ces 2 plugins dans votre ficher Maven (pom.xml) :

<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
	    	<plugin>
      			<groupId>com.diffplug.spotless</groupId>
      			<artifactId>spotless-maven-plugin</artifactId>
      			<version>2.43.0</version>
      			<configuration><java><googleJavaFormat/></java></configuration>
    		</plugin>
    		<plugin>
      			<groupId>org.apache.maven.plugins</groupId>
      			<artifactId>maven-checkstyle-plugin</artifactId>
      			<version>3.3.1</version>
      			<configuration><configLocation>google_checks.xml</configLocation></configuration>
    		</plugin>
  		</plugins>
	</build>

.gitlab-ci.yml :

stages:
  - lint
  - test
  - build

test-code:
  stage: lint
  image: maven:3.9-eclipse-temurin-17     # répété 3 fois pour la démo, on va simplifier après avec "default"
  script:
    - mvn -B -ntp checkstyle:check spotless:check 

test-unitaire:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn -B -ntp test

maven-build:
  stage: build
  image: maven:3.9-eclipse-temurin-17
  script:
      - echo "Compilation et exécution build..."
      - mvn -B -ntp clean package -DskipTests
  artifacts:
      paths:
        - target/
      expire_in: 1 week

Pour le fun, voici la même classe que tout à l’heure mais en version pas clean !

alt text

Logiquement, votre code devrait s’arrêter au stage lint !

alt text

Que faire ?

Cliquez sur le job pour voir les logs et découvrir les soucis de code, Spotless n’est pas content ;((

alt text

alt text

Vous voyez dans les logs, qu’il vous conseille de faire un Run 'mvn spotless:apply' to fix these violations. Les fichiers seront reformatés au format Google Java Format.

Donc ici, on voit bien que Spotless a fait échoué le Job et vous propose corriger. Il a détecté 4 fichiers à reformater. Mais comment corriger ?

Première possibilité : En local (généralement sur votre propre branche)

mvn spotless:apply
mvn -q test     # ici, optionnel
git add -A
git commit -m "style: format code with Spotless"
git push

Ensuite, le pipeline drvait passer au vert (le CI vérifie avec spotless:check et les devs corrigent avec spotless:apply)

Seconde possibilité : intégré au CI (peu recommandé; juste pour la démo)

Voici une proposition de fichier gitlab-ci.yamlmodifié

stages:
  - lint
  - test
  - build
# plus simple je mets une seule fois l'image docker
default:
  image: maven:3.9-eclipse-temurin-17

# option 1 : style "puriste", on vérifie seulement (échoue si moche !)
lint:
  stage: lint
  script:
    - mvn -B -ntp checkstyle:check spotless:check

# option 2 : style "auto-fix" en CI (passe même si le repo n’est pas re-commité !)
lint-autofix:
  stage: lint
  script:
    - mvn -B -ntp spotless:apply   # corrige localement dans le job
    - mvn -B -ntp checkstyle:check spotless:check

test-unitaire:
  stage: test
  script:
    - mvn -B -ntp test

maven-build:
  stage: build
  script:
      - echo "compilation et exécution build..."
      - mvn -B -ntp clean package -DskipTests   # ici, j'ai ajouté un skipTests puisqu'on les a déjà fait dans le job précédent
  artifacts:
      paths:
        - target/
      expire_in: 1 week

On peut éventuellement ajouter la création de rapports des tests unitaires de JUnit dans notre Job “test-unitaire” :

test-unitaire:
  stage: test
  script:
    - mvn -B -ntp test
  artifacts:
    when: always
    reports:
      junit: target/surefire-reports/*.xml
    paths:
      - target/surefire-reports/
    expire_in: 1 week

Rappel sur la commande mvn -B -ntp test

Option Signification Utilité
mvn Exécute Maven gestionnaire de build Java
-B Batch mode empêche les invites interactives (obligatoire en CI)
-ntp No Transfer Progress désactive la barre de progression (logs plus propres)
test Phase du cycle Maven exécute les tests unitaires via le plugin Surefire

Les étapes lorsque vous éxécutez mvn test :

  1. Compile ton code (compile)
  2. Compile les tests (test-compile)
  3. Exécute les classes de test dans src/test/java grâce au plugin maven-surefire-plugin
  4. Génère les rapports de test dans : target/surefire-reports/

Remarque : Vous avez remarqué que lors de la création de votre projet avec Spring Initializr, Spring Boot génére une classe de test d’intégration par défaut pour votre projet. Il est exécuté avec @SpringBootTest qui charge le contexte Spring complet. Même vide, gardez-le ! Il permet le test de démarrage global (intégration). Tandis que notre classe PasBeauControllerTest est un test unitaire classique qui n’utilise pas Spring Boot, on vérifie la logique interne.

Si vous enlevez la classe Tp2ApplicationTests.java, tout fonctionnera mais si un @Bean est mal configuré, si application.properties contient une erreur de port ou de datasource, si une dépendance manquante empêche le démarrage de Spring, vous ne le verrez qu’au moment du run réel, pas au test !

Retenez que votre mvn test exécutera les 2 !

Exemple :

package fr.exemple.gitlab.tp2;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class Tp2ApplicationTests {

	@Test
	void contextLoads() {
	}
}

Nos 2 tests n’ont pas le même rôle !

Classe de test Type de test Objectif
Tp2ApplicationTests test d’intégration minimal vérifier que le contexte Spring Boot démarre sans erreur
PasBeauControllerTest test unitaire (pur Java) vérifier que le contrôleur renvoie bien la valeur attendue

Schéma pour résumer

                ┌─────────────────────────────┐
                │         .gitlab-ci.yml      │
                └─────────────────────────────┘
                              │
                              ▼
          ┌────────────────────────────────────────┐
          │                Pipeline CI/CD          │
          └────────────────────────────────────────┘
                              │
      ┌───────────────────────┼────────────────────────┐
      ▼                       ▼                        ▼
 ┌────────────┐          ┌────────────┐           ┌────────────┐
 │  LINT      │          │   TEST     │           │   BUILD    │
 │ (Checkstyle│          │ (JUnit 5)  │           │ (Packaging)│
 │ + Spotless)│          │            │           │            │
 └────────────┘          └────────────┘           └────────────┘
      │                       │                        │
      │                       │                        │
      ▼                       ▼                        ▼
 ┌────────────┐       ┌────────────────────┐      ┌────────────────┐
 │ Vérifie le │       │ Exécute les tests  │      │ Produit le JAR │
 │ style du   │       │ unitaires et       │      │ exécutable     │
 │ code Java  │       │ d’intégration      │      │ (target/*.jar) │
 │ (formatage)│       │ avec Maven Surefire│      │                │
 └────────────┘       └────────────────────┘      └────────────────┘
      │                       │                        │
      ▼                       ▼                        ▼
   ✅ ou ❌               ✅ ou ❌                 ✅ ou ❌
  (style ok ?)        (tests réussis ?)       (build sans erreur ?)