Dans cette troisième partie nous allons enrichir le modèle en prenant en compte des contraintes.

La technique utilisée est la validation par annotation via la jsr 303 : Bean Validation. L’implémentation retenue est celle d’hibernate : hibernate-validator qui est l’implémentation de référence de la jsr.

Dépendances maven à ajouter dans le pom.xml du projet :

<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-validator</artifactId>
	<version>4.0.2.GA</version>
</dependency>
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-api</artifactId>
	<version>1.6.1</version>
</dependency>

On suppose que les spécifications demandent l’ajout des contraintes de validation suivantes :

  • Code produit doit être non null
  • Nom produit doit être non null
  • Longueur nom produit ne doit pas dépasser 20 caractères

On écrit les tests de validation dans ProduitTest :

private static Validator validator;

@BeforeClass
public static void setUp() {
	ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
	validator = factory.getValidator();
}

@Test
public void codeIsNullAlorsViolationContrainte() {
	Produit produitSansCode = produitAyantCode(null).nom("nouveau produit")
			.build();
	assertViolationContrainte(produitSansCode, "ne peut pas être nul");
}

@Test
public void nomIsNullAlorsViolationContrainte() {
	assertViolationContrainte(produitAyantCode("A").build(),
			"ne peut pas être nul");
}

@Test
public void nomPlusDe20CaracteresAlorsViolationContrainte() {
	Produit produit = produitAyantCode("A").nom(
			"nom plus de vingt caractères").build();
	assertViolationContrainte(produit, "la taille doit être entre 0 et 20");
}

private <T> void assertViolationContrainte(T obj, String message) {
	Set<ConstraintViolation<T>> constraintViolations = validator
			.validate(obj);
	assertEquals(1, constraintViolations.size());
	assertEquals(message, constraintViolations.iterator().next()
			.getMessage());
}

Pour faire passer les tests on annote la classe Produit :

private static final int TAILLE_MAX_NOM = 20;

@NotNull
private String code;

@NotNull
@Size(max = TAILLE_MAX_NOM)
private String nom;

C’est vert. On peut livrer mais il reste quand même une dette sur le modèle, il y a un todo sur la méthode equalsAuxLimites, il faudrait externaliser la méthode en tant que méthode static dans une classe ModelHelper.

Après refactoring on obtient pour la classe Produit :

public class Produit {

	private static final int DELAI_MOIS_MISE_LIGNE = 6;

	private static final int ODD_NUMBER = 31;

	private static final int TAILLE_MAX_NOM = 20;

	@NotNull
	private String code;

	@NotNull
	@Size(max = TAILLE_MAX_NOM)
	private String nom;

	private boolean innovant = true;

	private LocalDate dateMiseEnLigne = new LocalDate()
			.plusMonths(DELAI_MOIS_MISE_LIGNE);

	//omission des méthodes get et set pour lisibilité de l'exemple
        //...

	@Override
	public int hashCode() {
		return new HashCodeBuilder(ODD_NUMBER, 1)
                               .append(code).toHashCode();
	}

	@Override
	public boolean equals(final Object obj) {
		if (ModelHelper.isInstanceOf(obj, Produit.class)) {
			Produit other = (Produit) obj;
			return ModelHelper.equals(this, obj,
					new EqualsBuilder().append(this.code, other.code));
		}
		return false;
	}

}

et pour la classe ModelHelper on a :

public class ModelHelper {

	public static boolean isInstanceOf(final Object obj,
			Class objectClass) {
		return obj != null && objectClass.isInstance(obj);
	}

	public static boolean equals(final Object notNullObject,
			final Object other, EqualsBuilder equalsBuilder) {
		if (notNullObject == other) {
			return true;
		} else if (other == null) {
			return false;
		} else if (notNullObject.getClass() != other.getClass()) {
			return false;
		} else {
			return equalsBuilder.isEquals();
		}
	}
}

On refactore également les tests et on obtient finalement pour ProduitTest :

public class ProduitTest {

	private static Validator validator;

	private static final LocalDate TRENTE_MARS_2012 = new LocalDate(2012, 3, 30);

	@BeforeClass
	public static void setUp() {
		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
		validator = factory.getValidator();
	}

	@Test
	public void builder() {
		Produit produit = produitAyantCode("A").nom("builder").innovant(false)
				.miseEnLigne(TRENTE_MARS_2012).build();
		assertEquals("A", produit.getCode());
		assertEquals("builder", produit.getNom());
		assertEquals(TRENTE_MARS_2012, produit.getDateMiseEnLigne());
		assertFalse(produit.isInnovant());
	}

	@Test
	public void nouveauProduitEstInnovant() {
		assertTrue(new Produit().isInnovant());
	}

	@Test
	public void nouveauProduitEstDispoDans6Mois() {
		assertEquals(new LocalDate().plusMonths(6),
				new Produit().getDateMiseEnLigne());
	}

	@Test
	public void deuxProduitMemeCodeSontEgaux() {
		assertEquals(
				produitAyantCode("A").nom("produit top")
						.miseEnLigne(TRENTE_MARS_2012).build(),
				produitAyantCode("A").nom("produit super").innovant(false)
						.build());
	}

	@Test
	public void deuxProduitCodesDifferentsNonEgaux() {
		assertFalse(produitAyantCode("A").nom("nom produit A").build()
				.equals(produitAyantCode("B").nom("nom produit A").build()));
	}

	@Test
	public void equals_InstanceDifferentProduitAlorsFalse() {
		assertFalse(new Produit().equals(new Object()));
	}

	@Test
	public void deuxProduitsEgauxOntMemeHashCode() {
		// respect du contrat general methode hashcode
		// pre-assert
		assertEquals(produitAyantCode("A").build(), produitAyantCode("A")
				.build());
		assertFalse(produitAyantCode("A").build() == produitAyantCode("A")
				.build());

		// assert
		assertTrue(produitAyantCode("A").build().hashCode() == produitAyantCode(
				"A").build().hashCode());
	}

	@Test
	public void deuxProduitsDifferentsOntHashCodeDifferents() {
		// pour des raisons d'optimisation
		assertFalse(produitAyantCode("A").build().hashCode() == produitAyantCode(
				"B").build().hashCode());
	}

	@Test
	public void codeIsNullAlorsViolationContrainte() {
		Produit produitSansCode = produitAyantCode(null).nom("nouveau produit")
				.build();
		assertViolationContrainte(produitSansCode, "ne peut pas être nul");
	}

	@Test
	public void nomIsNullAlorsViolationContrainte() {
		assertViolationContrainte(produitAyantCode("A").build(),
				"ne peut pas être nul");
	}

	@Test
	public void nomPlusDe20CaracteresAlorsViolationContrainte() {
		Produit produit = produitAyantCode("A").nom(
				"nom plus de vingt caractères").build();
		assertViolationContrainte(produit, "la taille doit être entre 0 et 20");
	}

	private <T> void assertViolationContrainte(T obj, String message) {
		Set<ConstraintViolation<T>> constraintViolations = validator
				.validate(obj);
		assertEquals(1, constraintViolations.size());
		assertEquals(message, constraintViolations.iterator().next()
				.getMessage());
	}

}

Et enfin ModelHelperTest :

public class ModelHelperTest {

	@Test
	public void isInstanceOf() {
		assertFalse(ModelHelper.isInstanceOf(null, Produit.class));
		assertFalse(ModelHelper.isInstanceOf("a string", Produit.class));
		assertTrue(ModelHelper.isInstanceOf(
				ProduitBuilder.produitAyantCode("code").build(), Produit.class));
	}

	@Test(expected = NullPointerException.class)
	public void equals_premierObjetNull_AlorsNPE() {
		// setup
		Produit produit = ProduitBuilder.produitAyantCode("code").build();

		// test && assert
		ModelHelper.equals(null, produit,
				new EqualsBuilder().append(null, produit.getCode()));
	}

	@Test
	public void equals() {
		// setup
		Produit produit = ProduitBuilder.produitAyantCode("code").build();
		Produit autreProduit = ProduitBuilder.produitAyantCode("code").build();
		EqualsBuilder equalsBuilder = new EqualsBuilder().append(
				produit.getCode(), autreProduit.getCode());

		// test && assert
		assertTrue(ModelHelper.equals(produit, autreProduit, equalsBuilder));
		assertTrue(ModelHelper.equals(produit, produit, equalsBuilder));
		assertFalse(ModelHelper.equals(produit, null, equalsBuilder));
		assertFalse(ModelHelper.equals(produit, new Object(), equalsBuilder));
		assertFalse(ModelHelper.equals(produit, new Produit() {
		}, equalsBuilder));
	}
}

Nous sommes allés plus loin que de coder directement une classe java, mais avec un effort modéré on perçoit déjà le gain en fiabilité et productivité  pour  la gestion de la persistance et les services métier futurs. En fait un bon nombre de préoccupations et de pratiques que l’on retrouve souvent dupliquées dans les différentes couches sont cadrées dans le coeur du métier. Un retour sur l’investissement en temps est déjà là puisque le design a fait émergé un ModelHelper apportant des fonctionnalités génériques au modèle, de plus les tests unitaires sont nettement plus faciles à écrire que si aucun effort n’avait été fait. En fin de compte une prise de recul au bon moment c’est beaucoup de temps gagné pour la suite avec une mise de départ très raisonnable.

Pour finir quelques indicateurs sur ce qui a été développé :

2 fonctionnalités développés (certes simples mais quand même !)

  • nouveauProduitEstInnovant
  • nouveauProduitEstDispoDans6Mois

3 contraintes garanties

  • codeIsNullAlorsViolationContrainte
  • nomIsNullAlorsViolationContrainte
  • nomPlusDe20CaracteresAlorsViolationContrainte

Unicité et comportement dans les collections garanties

  • deuxProduitMemeCodeSontEgaux
  • deuxProduitCodesDifferentsNonEgaux
  • deuxProduitsEgauxOntMemeHashCode
  • deuxProduitsDifferentsOntHashCodeDifferents

Classe ProduitTest

  • 39 lignes de code (dont 4 externalisables)

Classe Produit

  • 18 lignes de codes (hors accesseurs)
  • 1 bloc if
Classe ProduitBuilder réutilisable pour les tests sur modèle, persistance et services

ModelHelper :  2 utilitaires génériques pour le modèle métier

boolean isInstanceOf(final Object obj,Class<? extends Object> objectClass)

boolean equals(final Object notNullObject, final Object other, EqualsBuilder equalsBuilder)


Publicités