EasyMock – Techniques avancées

Easymock est un framework de test offrant une API permettant de tester plus facilement le code. Cet article ne constitue pas une présentation d’easymock et s’adresse aux développeurs souhaitant progresser sur l’utilisation de cette API.

Lorsque l’on met en place les mocks, il est fréquent que l’on ait besoin de tester qu’un objet créé à l’intérieur de la méthode testée correspond bien à ce que l’on attend, cet objet étant passé en tant que paramètre à une méthode d’un objet mocké.

Prenons pour exemple, la classe Service et sa méthode processRequest. La méthode processRequest prend en entrée deux paramètres et se sert de ces paramètres pour créer un objet Bond, qui sera passé ensuite à la méthode persist du service BondDao.

package fr.java.freelance.easymock;

import java.math.BigDecimal;

public class Service {
	private BondDao bondDao;

	public String processRequest(String name,BigDecimal quantity) {
		Bond bond = new Bond(name,quantity);
		return bondDao.persist(bond);
	}

	public void setBondDao(BondDao bondDao) {
		this.bondDao = bondDao;
	}
}

package fr.java.freelance.easymock;

public interface BondDao {
	String persist(Bond bond);
}

package fr.java.freelance.easymock;

import java.math.BigDecimal;

public class Bond {
	private String name;
	private BigDecimal quantity;

	public Bond(String name, BigDecimal quantity) {
		super();
		this.name = name;
		this.quantity = quantity;
	}
	public String getName() {
		return name;
	}
	public BigDecimal getQuantity() {
		return quantity;
	}
}

Il est a priori impossible pour un test unitaire d’accèder à l’objet « bond » pour le vérifier.

Ce que l’on cherche à garantir, c’est que la méthode processRequest construit un objet de type Bond ayant comme attributs name et quantity les 2 valeurs passées en paramètres, qu’elle le transmette à la méthode persist de ServiceDao et retourne le paramètre de retour de cette méthode persist.

Utilisation d’EasyMock.anyObject()

La première méthode pour tester ceci est d’utiliser la méthode anyObject de l’objet EasyMock.

Service service = new Service();
BondDao bondDaoMock = EasyMock.createMock(BondDao.class);
service.setBondDao(bondDaoMock);

final String persist_return = "123AX";
final String name = "Name 5%";
final BigDecimal quantity = BigDecimal.TEN;

EasyMock.expect(bondDaoMock.persist((Bond) EasyMock.anyObject()))
.andReturn(persist_return);

EasyMock.replay(bondDaoMock);
String idInternal = service.processRequest(name, quantity);
Assert.assertEquals(persist_return,idInternal);
EasyMock.verify(bondDaoMock);

Le test fonctionne, néanmoins rien ne garantit que l’objet que l’on transmet à la méthode persist de BondDao est conforme à ce que l’on attend.

Pour pouvoir analyser l’objet Bond transmis, il existe plusieurs possibilités, dont une utilisant la redéfinition de la méthode equals de l’objet Bond et deux fournies par la librairie EasyMock, l’utilisation d’un IArgumentMatcher ou de EasyMock.capture.

Rédéfinition de la méthode equals()

Supposons que l’objet bond redéfinisse la méthode equals de manière à tester l’égalité des paramètres name et quantity.

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Bond other = (Bond) obj;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (quantity == null) {
			if (other.quantity != null)
				return false;
		} else if (quantity.compareTo(other.quantity)!=0)
			return false;
		return true;
	}

Il suffit alors d’écrire le test suivant pour garantir que l’objet passé à la méthode persist est bien égal au bond attendu :

        @Test
	public void testProcessRequestWithEquals() {
		Service service = new Service();
		BondDao bondDaoMock = EasyMock.createMock(BondDao.class);
		service.setBondDao(bondDaoMock);

		final String persist_return = "123AX";
		final String name = "Name 5%";
		final BigDecimal quantity = BigDecimal.TEN;
		final Bond bondExpected = new Bond(name,quantity);

		EasyMock.expect(bondDaoMock.persist(bondExpected))
				.andReturn(persist_return);
		EasyMock.replay(bondDaoMock);
  	        String idInternal = service.processRequest(name, quantity);
		Assert.assertEquals(persist_return,idInternal);
		EasyMock.verify(bondDaoMock);
	}

Cette démarche est la plus naturelle, néanmoins elle ne s’applique pas dans le cas où la méthode equals n’est n’ai pas redéfinie (dans ce cas, c’est la méthode equals de Object qui est prise en compte et donc, les deux objets ne seraient pas égaux (pas la même instance)). Elle ne s’applique pas non plus dans le cas où la méthode equals ne correspondrait pas à ce que l’on a définit comme étant utile pour tester l’égalité. Par exemple une méthode equals qui ne testerait que l’égalité de l’attribut name dans l’objet Bond alors que nous souhaitons garantir les valeurs des deux paramètres. On ne peut évidement pas modifier la méthode equals d’un objet pour le tester, cela changerai son comportement !!
Il faut donc se tourner vers d’autres solutions.

Utilisation d’un IArgumentMatcher

EasyMock permet d’utiliser un certain nombre de matcher déjà définit ( eq, isNull, matches ..). Néanmoins dans le cas présent, il nous faut définir notre propre matcher pour pouvoir tester l’égalité des paramètres name et quantity.
La redéfinition d’un matcher s’effectue en deux temps.
D’abord, il faut créer une classe implémentant IArgumentMatcher.

package fr.java.freelance.easymock;

import org.easymock.EasyMock;
import org.easymock.IArgumentMatcher;

public class BondEquals implements IArgumentMatcher {
	private final Bond expected;

	public BondEquals(Bond expected) {
		this.expected = expected;
	}

	public boolean matches(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (expected.getClass() != obj.getClass())
			return false;
		Bond other = (Bond) obj;
		if (this.expected.getName() == null) {
			if (other.getName() != null)
				return false;
		} else if (!this.expected.getName().equals(other.getName()))
			return false;
		if (this.expected.getQuantity() == null) {
			if (other.getQuantity() != null)
				return false;
		} else if (this.expected.getQuantity().compareTo(other.getQuantity()) != 0)
			return false;
		return true;
	}

	public void appendTo(StringBuffer buffer) {
		buffer.append("eqException ").append(expected.toString());
	}

	public static Bond eqBond(Bond in) {
	    EasyMock.reportMatcher(new BondEquals(in));
	    return null;
	}
}

Pour pouvoir utiliser le BondEquals ainsi crée, la méthode persist de l’interface BondDao n’acceptant que les objets de type Bond, on utilise l’astuce suivante :

public static Bond eqBond(Bond in) {
	    EasyMock.reportMatcher(new BondEquals(in));
	    return null;
	}

On peut alors utiliser le test suivant, pour garantir d’une part le bon retour de la méthode processRequest ainsi que le passage du bond ayant les caractéristiques souhaitées à la méthode persist du BondDao.

        @Test
	public void testProcessRequestWithArgumentMatcher() {
		Service service = new Service();
		BondDao bondDaoMock = EasyMock.createMock(BondDao.class);
		service.setBondDao(bondDaoMock);

		final String persist_return = "123AX";
		final String name = "Name 5%";
		final BigDecimal quantity = BigDecimal.TEN;

		final Bond bondExpected = new Bond(name,quantity);
		EasyMock.expect(bondDaoMock.persist(BondEquals.eqBond(bondExpected)))
				.andReturn(persist_return);
		EasyMock.replay(bondDaoMock);
		String idInternal = service.processRequest(name, quantity);
		Assert.assertEquals(persist_return,idInternal);
		EasyMock.verify(bondDaoMock);
	}

Utilisation de org.easymock.Capture

Une autre méthode consiste à capturer l’objet bond passé à la méthode persist du DAO et de faire des tests dessus dans un deuxième temps.
Une capture s’effectue en 3 temps.
1. Déclaration de la capture : Capture capture = new Capture();
2. Capture du paramètre lors de l’exécution via : EasyMock.expect(bondDaoMock.persist(EasyMock.and(EasyMock.isA(Bond.class), EasyMock.capture(capture))))
3. Récupération de l’objet capturé : Bond captured = capture.getValue();

	@Test
	public void testProcessRequestWithCapture() {
		Service service = new Service();
		BondDao bondDaoMock = EasyMock.createMock(BondDao.class);
		service.setBondDao(bondDaoMock);

		final String persist_return = "123AX";
		final String name = "Name 5%";
		final BigDecimal quantity = BigDecimal.TEN;

               Capture capture = new Capture();

		EasyMock.expect(bondDaoMock.persist(EasyMock.and(EasyMock
                          .isA(Bond.class), EasyMock.capture(capture))))
                .andReturn(persist_return);

		EasyMock.replay(bondDaoMock);
		String idInternal = service.processRequest(name, quantity);
		Assert.assertEquals(persist_return,idInternal);

		Bond captured = capture.getValue();
		Assert.assertEquals(name,captured.getName());
		Assert.assertTrue(quantity.compareTo(captured.getQuantity())==0);
		EasyMock.verify(bondDaoMock);
	}

Il existe également d’autres méthodes utilisant des outils externes pour atteindre un but similaire mais je n’ai pas encore trouvé de limitation à la création d’un nouveau matcher ou la capture de l’élément à tester. Si il y a besoin plusieurs fois de tester l’objet de manière identique, j’ai tendance à créer un nouveau matcher et à utiliser la capture dans le cadre d’objets plus petits ou de besoin spécifique à un test.

5 Responses to “EasyMock – Techniques avancées”

  1. David dit :

    La même chose avec Mockito. Un peu moins verbeux à mon avis.

    http://pastie.org/864826

  2. Tasha dit :

    Merci beaucoup! J’étais justement en train de regarder EasyMock! Very helpful :)

  3. Jean-Baptiste dit :

    Merci, et effectivement, Mockito est définitivement plus élégant !
    Note pour plus tard : faire un article « Pourquoi je préfère Mockito à EasyMock »

  4. Tasha dit :

    JB, n’hésites pas (trop longtemps) ;)

  5. Nabil dit :

    Merci Mathilde pour cet exemple, c’est très fréquent comme besoin.

    Je préfère par contre la syntaxe mockito qui est effectivement moins verbeuse.
    Nabil

Leave a Reply