Spring : la validation simplifiée avec Spring. Démos pratiques.

Ce post a pour objectif de vous faire découvrir les nouveautés de Spring bean validation capable dorénavant de supporter les  JSR 303 & 349. En exemple, nous verrons comment réaliser des tests unitaires et des tests d'intégration (où le validator est injecté par spring) et nous rédigerons des tests d'intégration dans lesquels nous abuserons des nouvelles annotations de spring-boot. A titre d'exemple, nous verrons également comment employer les annotations @RunWith(SpringRunner.class) et @SpringBootTest.

Deux manières d'activer la validation

L'exemple du projet MAVEN

Le projet MAVEN démo ci-après s'appuie sur javax.validation.Validator ou org.springframework.validation.Validator et son  adaptateur SpringValidatoradapter (spring 4),  l'api validation-api-1.1.0.Final et son provider hibernate-validator 5.2.4.Final.

Le projet compile juste avec validation-api-1.1.0.Final.

A l'exécution la présence d'un provider (tel hibernate-validator-xxx.jar) devient nécessaire.

La démo s'appuie également sur le framework lombok et java 8.

 

Pour démarrer en douceur, la première partie de la démo utilise l'interface javax.validation.Validator uniquement et bean (POJO) ayant des attributs annotés pour validation.

Dans celle-ci nous présentons deux versions de test : Test unitaire et un autre d'intégration.

La seconde partie de la démo utilise SpringValidatorAdapter exclusivement ce qui nous permet de personnaliser les messages d'erreurs liées à la validation.

Aussi deux versions sont présentées : test unitaire et d'intégration avec Junit et spring-boot-test.

Enfin, si le temps le permet,  une troisième partie de la démo présente une façon de pluguer un validator pour tester des beans non annotés.

En pratique !

Premier test - Part 1

Là aussi contrairement à nos habitudes, commençons par écrire le premier test :

package fr.netapsys.abdou.validate;
import static org.junit.Assert.assertEquals;
import java.util.Set;
import javax.validation.*;
import javax.validation.groups.Default;
import org.junit.*;;
import fr.netapsys.abdou.validate.Montants;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TUDefaultValidatorTest {
	private static Validator validator;
	private Set<ConstraintViolation<Montants>> violations;
	
 @BeforeClass
 public static void setUp() {
   ValidatorFactory factory =
     Validation.buildDefaultValidatorFactory();
		validator = factory.getValidator();
 }
@Test
 public void testMontantOK() {
   Montants montants = new Montants();		
   montants.setStrMontant("-231,90");
   violations = 
   validator.validate(montants);
   assertEquals(0, violations.size());
 }
@Test 
public void testMontantInValids() {
 Montants montants = new Montants();		
 montants.setStrMontant("23197Z");
 violations = 
 validator.validate(montants,Default.class);
 log.info("erreurs validation:"+violations);
 assertEquals(1, violations.size());
 }
}

Il est utile de remarquer l'annotation @Slf4j de lombok qui nous permet de tracer les logs des erreurs ou violations de validation.

Ce test unitaire a pour but de valider les propriétés du POJO Montants:

package fr.netapsys.abdou.validate;
import java.io.Serializable;
import javax.validation.constraints.*;
import lombok.Data;

@Data
public class Montants implements Serializable {
	
 @Pattern( regexp="^([+-])?[0-9]+([,.][0-9]{1,2})?", 
 message="montant ${validatedValue} invalide. Il doit suivre ######,## ou #####.##")
 private String strMontant;

@DecimalMax(value=IAvantage.AVANTAGE_MAX, 
  message="L'avantage ${formatter.format('%1$.2f', validatedValue)}
 ne doit pas exceder {value}")
  private double avantage;

}

Notez la présence des annotations de validation: @Pattern, DecimalMax du package javax.validation.

Notez également l'annotation @Data de lombok qui génère entre autres les setters/getters et toString utiles.

La propriété strMontant de type String doit être conforme au pattern fourni pour qu'elle soit considérée valide.

L'attribut avantage de type Double doit être en dessous du max précisé (ici c'est IAvantage.AVANTAGE_MAX valorisé à 200.0 comme le montre l'extrait du code ci-après).

Justement l'interface IAvantage est simple :

package fr.netapsys.abdou.validate;

public interface IAvantage {
	public static final String AVANTAGE_MAX = "200.0";

	double getAvantage();
	void setAvantage(double av);
}

La question : Que fait exactement ce premier test unitaire ?

Avant de répondre voyons le résultat de l'exécution du test unitaire JUnit:

..
[main] INFO o.h.validator.internal.util.Version - HV000001: 
 Hibernate Validator 5.2.4.Final
[main] INFO fr.n.a.validate.TUDefaultValidatorTest - erreurs validation:

[ConstraintViolationImpl{interpolatedMessage='montant 23197Z invalide. 
 Il doit suivre ######,## ou #####.##',
propertyPath=strMontant, rootBeanClass=class f.n.a.validate.Montants, 
messageTemplate='montant ${validatedValue} invalide. 
 Il doit suivre ######,## ou #####.##'}]
traces de mvn test

Dans la méthode annotée avec  @BeforeClass, on instancie un validator avec la factory buildDefaultValidatorFactory qui utilise par défaut l'implémentation hibernate-validator si le jar du provider est présent dans le classpath.

Dans la première méthode nommée testMontantOK, on teste l'attribut valorisé avec la valeur "-231.90".

Le test montre que cette valeur est valide puisque l'assertion sur la taille des violations est à zéro.

La seconde méthode 'testMontantInValids' permet de voir un exemple de valeur rejetée (car "23197Z" contient la lettre Z).

Vous remarquez que les messages de violation des contraintes de validation sont explicites :

Nous avons le nom de l'attribut en erreur ainsi que la valeur rejetée!

Premier test - Part 2

Le second volet de ce premier test est de réaliser un test d'intégration. Voici le code java du test TI:

package fr.netapsys.abdou.validate.defaults.montants;
import static org.junit.Assert.*;
import java.util.Set;
import javax.validation.*;
import javax.validation.groups.Default;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import fr.netapsys.abdou.validate.*;
import lombok.extern.slf4j.Slf4j;

@RunWith(SpringRunner.class)
@SpringBootTest(classes={DemoValidatorAvanceApplication.class})
@Slf4j
public class TIntegPatternByDefaultValidatorTest {
@Autowired
private Validator validator;
private Set<ConstraintViolation<Montants>> violations;
 @Test
 public void testMontantInValid() {
	Montants m = new Montants();
	m.setStrMontant("23197Z");
	violations = validator.validate(m);
	log.info("violations:"+violations);
	assertEquals(1, violations.size());
 }
}

La différence ici sont celles-ci :

Comme il s'agit d'un test d'intégration, notez la présence de ces deux annotations

@RunWith(SpringRunner.class)
@SpringBootTest(classes={DemoValidatorAvanceApplication.class})

Ensuite, spring injecte un validator par défaut via la ligne :

@Autowired private Validator validator;

Ici c'est le contrat javax.validation.Validator qui est utilisé.

Spring va chercher dans la classe java annotée avec @Configuration (ici c'est ValidatorConfigApp.java ) le bean de type javax.validation.Validator.

Justement, voici l'extrait du code qui nous intéresse dans la classe ValidatorConfigApp:

package fr.netapsys.abdou.validate;
import javax.validation.*;
import org.s.context.annotation.Bean;
import org.s.context.annotation.Configuration;

@Configuration
public class ValidatorConfigApp {

  @Bean
  public Validator validator(){
 	return 
  
  Validation.buildDefaultValidatorFactory()
      .getValidator();
  }
//....
}

C'est la méthode validator() qui renvoie le bean qui est injecté par spring via @Autowired.

L'assertion du test est identique à celle déjà vérifié dans le test unitaire.

Second test

L'essentiel de ce qu'il faudrait connaître sur javax.validation est donc clair, nous sommes en mesure de passer au Validator de spring.

La partie II de la démo utilise ainsi exclusivement org.springframework.validation.Validator de spring en test d'intégration uniquement.

Voici le code du second test d'intégration:

package fr.netapsys.abdou.validate.montants.springinject;
import static org.junit.Assert.assertEquals;
import java.util.Set;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import javax.validation.ConstraintViolation;
import javax.validation.groups.Default;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import fr.netapsys.abdou.validate.DemoValidatorAvanceApplication;
import fr.netapsys.abdou.validate.Montants;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes={DemoValidatorAvanceApplication.class})
public class TIntegValidatorSpringAdapterTest {

@Autowired 
private SpringValidatorAdapter springValidatorAdapter;
	
 @Test
 public void testMontantInvalidWithSpring() {
  Montants montants = new Montants();
  montants.setStrMontant("45Z");
  Set<ConstraintViolation<Montants>> violations = 
    springValidatorAdapter.validate(montants,Default.class);
  log.info("violations:"+violations);
  assertEquals(1, violations.size());
 }
}

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *