mercredi 5 novembre 2014

Injecter des mocks dans un objet sans impacter le code pour le test en utilisant la reflexion

Problème

Comment injecter des mocks sur une classe contenant des champs privés sans modifier cette classe en y ajoutant un setter ou un constructeur spécialisé ???!

Solution

Au tout début ... 

 

Dépendances nécessaire:

‹dependencies›
  ‹dependency›
   ‹groupid›junit‹/groupid›
   ‹artifactid›junit‹/artifactid›
   ‹version›4.5‹/version›
   ‹scope›test‹/scope›
  ‹/dependency›
  ‹dependency›
   ‹groupid›org.mockito‹/groupid›
   ‹artifactid›mockito-all‹/artifactid›
   ‹version›1.9.5‹/version›
   ‹scope›test‹/scope›
  ‹/dependency›
‹/dependencies› 


Soit la classe MyClass :

package fr.ftravaglia.demo.mokitoinjection;

/**
 * Use for test with reflexion
 * @author ftravaglia
 */
public class MyClass {
 
 // Field that haven't setter !
 private transient Foo myFoo;
 
 /**
  * Default constructor
  */
 public MyClass(){
  super();
  myFoo = new Foo();
 }

 /**
  * Return the Foo name.
  * @return
  */
 public String printMyFooName(){
  return myFoo.printMyName();
 }
}

Et la classe Foo :

package fr.ftravaglia.demo.mokitoinjection;

/**
 * Object to mock
 * @author ftravaglia
 */
public class Foo {

 /**
  * Default constructor
  */
 public Foo(){
  super();
 }
 
 /**
  * Method used for demonstrate mock
  * @return
  */
 public String printMyName(){
  return this.getClass().getName();
 }
}

On veut mocker le résultat de 'printMyName' de la classe Foo par 'MyMockedResult' et injecter Foo dans MyClass sans utiliser de setter.

Utilisons un test pour valider le résultat :



Le test plante bien ce qui est normal :)


Utilisation de la réflexion

Dans les sources contenant le code des test implémenter une classe permettant de faire la réflexion (on pourrait faire sans la classe mais dans le but de réutiliser ce code dans les test c'est mieux :) )


package fr.ftravaglia.demo.mokitoinjection.util;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
 * Use reflexion for change value of attribut.
 * @author ftravaglia
 *
 */
public class InjectionHelper {

 /**
  * Set 'newFieldValue' as value for the field 'fieldName' on the object 'object'.
  * 
  * @param object
  * @param fieldName
  * @param newFieldValue
  * @throws SecurityException 
  * @throws NoSuchFieldException 
  * @throws IllegalAccessException 
  * @throws IllegalArgumentException 
  */
 public static void setField(Object object, String fieldName, Object newFieldValue) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
  final Field field = object.getClass().getDeclaredField(fieldName);
  field.setAccessible(true);

  // Use for modify the final attribut
  final Field modifiersField = Field.class.getDeclaredField("modifiers");
  modifiersField.setAccessible(true);
  modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

  field.set(object, newFieldValue);
 }
}





Modification du test unitaire pour utiliser la reflexion avec mockito

package fr.ftravaglia.demo.mokitoinjection;

import junit.framework.Assert;

import org.junit.Test;
import org.mockito.Mockito;

import fr.ftravaglia.demo.mokitoinjection.util.InjectionHelper;

/**
 * Test the class {@link MyClass}
 * @author ftravaglia
 *
 */
public class MyClassTest {

 private transient MyClass myClass;
 private transient Foo foo;
 public MyClassTest() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{
  super();
  
  myClass = new MyClass();
  
  foo = Mockito.mock(Foo.class);
  InjectionHelper.setField(myClass, "myFoo", foo);
 }
 
 @Test
 public void printMyFooNameMustReturnMyMockedResult(){
  
  final String valueMock = "MyMockedResult";
  
  Mockito.when(foo.printMyName()).thenReturn(valueMock);
  Assert.assertEquals(valueMock, myClass.printMyFooName());
 }
}


On relance le test:





C'est vert !

Ca marche même avec les champs static final ;)

Code source dispo : https://github.com/ftravaglia/veille