Test against legacy code

Le projet sur lequel je travaille en ce moment n’est pas à la pointe des nouvelles techniques de développement. Un des problèmes qui nous rapidement dérangé est l’injection de dépendances. Pour récupérer un service (dans ce cas, une ejb2), on doit appeler une méthode statique qui va nous fournit une instance de l’ejb.

public interface OrderFacade {
	void createOrder(final Client client, final Order order) throws Exception;
}

/**
 * Legacy class used to retrieve the real implementation of a Facade. In the
 * current project at work, I don't really know it's really doing. Let's just
 * assume its create the facade implementation with a complex context.
 */
public class FacadeHelper {
	public static Object getService(final Class facadeInterface) {
		// Do some complex thing to retrieve implementation based on interface
		// from param.
		return new OrderFacadeImpl();
	}
}

public class OrderController {

	public boolean doSomethingWithOrder(final Client client, final Order order) {

		try {
			getOrderFacade().createOrder(client, order);
		} catch (Exception e) {
			// Remember : never swallow exception like this.
			// It's a real pain in production.
			return false;
		}

		return true;
	}

	/**
	 * We need to get an fresh facade implementation a each call. Don't really
	 * know the reason but I need.
	 */
	protected OrderFacade getOrderFacade() {
		return (OrderFacade) FacadeHelper.getService(OrderFacade.class);
	}
}

Le problème se situe dans l’écriture des tests unitaires. Pour pouvoir tester une méthode contre un mock, nous devons surcharger la classe et lui injecter le mock.

public class OrderControllerTest {

	/**
	 * Creating an internal class to inject the stub.
	 */
	private class OrderControllerMock extends OrderController {

		@Override
		protected OrderFacade getOrderFacade() {
			return new FailingToCreateOrderFacadeStub();
		}

	}

	@Test
	public void testDoSomethingWithOrder() {
		OrderController controller = new OrderControllerMock();

		boolean orderCreatingSucceded = controller.doSomethingWithOrder(
				new Client(), new Order());
		Assert.assertFalse(orderCreatingSucceded);
	}

}

Cette solution ne nous satisfaisait pas du tout. En alternative, nous avons d’abord penser à utiliser une librairie comme Powermock et mocker la méthode statique de récupération du service pour qu’elle revoie notre mock à la place. Malheureusement, nous sommes obligés d’utiliser junit 3 alors que powermock nécessite la version 4. Je précise que j’ai écrit les exemples de code chez moi où je n’ai aucune contrainte concernant les versions.

A la place, nous avons décidé d’utiliser des adapteurs afin de pouvoir injecter un mock lors des tests.

public interface ServiceAdapter {
	T getService();
}

public class LegacyServiceAdapter implements ServiceAdapter {

	private final Class type;

	public LegacyServiceAdapter(final Class type) {
		this.type = type;
	}

	@SuppressWarnings("unchecked")
	public T getService() {
		return (T) FacadeHelper.getService(type);
	}
}

public class InstanceServiceAdapter implements ServiceAdapter {

	private final T instance;

	public InstanceServiceAdapter(final T instance) {
		this.instance = instance;
	}

	public T getService() {
		return instance;
	}
}

Nous avons donc une interface qui fournit une méthode pour récupérer un service. Cette interface est implémenté par deux classes :

  • LegacyServiceAdapter : Est chargé de faire appel au FacadeHelper.
  • InstanceServiceAdapter : Prend directement une instance en paramétre. Va nous servir pour les tests.

Maintenant que nous avons ces adapteurs à notre disposition, il nous reste à modifier OrderController (la classe qui sert d’exemple).

public class NewOrderController {

	private final ServiceAdapter orderService;

	public NewOrderController() {
		orderService = new LegacyServiceAdapter(OrderFacade.class);
	}

	public NewOrderController(final OrderFacade orderFacade) {
		orderService = new InstanceServiceAdapter(orderFacade);
	}

	public boolean doSomethingWithOrder(final Client client, final Order order) {

		try {
			orderService.getService().createOrder(client, order);
		} catch (Exception e) {
			// Remember : never swallow exception like this.
			// It's a real pain in production.
			return false;
		}

		return true;
	}
}

La nouvelle version de notre controller propose deux constructeurs : le constructeur par défaut est en charge de la création de l’adapteur qui nous sert à récupérer une facade réelle; l’autre constructeur prend en paramètre une instance de la facade pour l’InstanceSerciveAdapter.

Le code du controler en est simplifier. Au lieu de créer une classe juste pour injecter le mock, nous pouvons maintenant fournir directement notre mock grâce au contructeur.

@Test
public void testDoSomethingWithOrder() {
	OrderFacade orderFacade = new FailingToCreateOrderFacadeStub();
	NewOrderController controller = new NewOrderController(orderFacade);
	boolean orderCreatingSucceded = controller.doSomethingWithOrder(
		new Client(), new Order());
	Assert.assertFalse(orderCreatingSucceded);
}