web-dev-qa-db-fra.com

Injection de valeur de printemps dans le mockito

J'essaie d'écrire une classe de test pour la méthode suivante

public class CustomServiceImpl implements CustomService {
    @Value("#{myProp['custom.url']}")
    private String url;
    @Autowire
    private DataService dataService;

J'utilise la valeur d'URL injectée dans l'une des méthodes de la classe. Pour tester cela, j'ai écrit une classe junit

@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext-test.xml" })
public CustomServiceTest{
    private CustomService customService;
    @Mock
    private DataService dataService;
    @Before
    public void setup() {
        customService = new CustomServiceImpl();
        Setter.set(customService, "dataService", dataService);
    }    
    ...
}

public class Setter {
    public static void set(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

Dans applicationContext-test.xml, je charge le fichier de propriétés à l'aide

    <util:properties id="myProp" location="myProp.properties"/>

Mais la valeur de l'URL n'est pas chargée dans le CustomService lors de l'exécution du test. Je me demandais s'il y avait un moyen de faire ça.

Merci

21
rohit

Vous pouvez câbler automatiquement un mutateur (setter), plutôt que d'annoter simplement le champ privé. Ensuite, vous pouvez également utiliser ce setter de votre classe de test. Pas besoin de le rendre public, le paquet privé fera comme Spring peut toujours y accéder, mais sinon seul votre test peut y entrer (ou tout autre code dans le même paquet).

@Value("#{myProp['custom.url']}")
String setUrl( final String url ) {
    this.url  = url;
}

Je ne suis pas un fan du câblage automatique différemment (par rapport à ma base de code) uniquement pour les tests, mais l'alternative de changer la classe en cours de test, à partir du test, est tout simplement impie.

3
Joseph Lust
import org.springframework.test.util.ReflectionTestUtils;

@RunWith(MockitoJUnitRunner.class)
public CustomServiceTest{

@InjectMock
private CustomServiceImpl customService;

@Mock
private DataService dataService;

@Before
public void setup() {
    ReflectionTestUtils.setField(customService, "url", "http://someurl");
}    
...
}
50
Robert Hutto

Je suis d'accord avec le commentaire de @skaffman.

Outre que votre test utilise le MockitoJUnitRunner, il ne recherchera donc aucun élément Spring, ce seul but est d'initialiser les mockito Mockito. Le ContextConfiguration ne suffit pas pour câbler les choses avec du ressort. Techniquement, avec JUnit, vous pouvez utiliser le runner suivant si vous voulez des trucs liés au printemps: SpringJUnit4ClassRunner.

De plus, lorsque vous écrivez un Test unitaire, vous voudrez peut-être reconsidérer l'utilisation du ressort. L'utilisation d'un câblage à ressort dans un test unitaire est incorrecte. Cependant, si vous écrivez à la place un Test d'intégration alors pourquoi utilisez-vous Mockito là-bas, cela n'a pas de sens (comme l'a dit skaffman)!

EDIT: Maintenant, dans votre code, vous définissez directement le CustomerServiceImpl dans votre bloc avant, cela n'a pas de sens non plus. Le printemps n'y est pas du tout impliqué!

@Before
public void setup() {
    customService = new CustomServiceImpl();
    Setter.set(customService, "dataService", dataService);
}

EDIT 2: Si vous voulez écrire un nit Test de CustomerServiceImpl, alors évitez Spring stuff et injectez directement la valeur de la propriété. Vous pouvez également utiliser Mockito pour injecter la variable fictive DataService dans l'instance testée.

@RunWith(MockitoJUnitRunner.class)
public CustomServiceImplTest{
    @InjectMocks private CustomServiceImpl customService;
    @Mock private DataService dataService;

    @Before void inject_url() { customerServiceImpl.url = "http://..."; }

    @Test public void customerService_should_delegate_to_dataService() { ... }
}

Comme vous l'avez peut-être remarqué, j'utilise un accès direct au champ url, le champ peut être visible par le package. Il s'agit d'une solution de contournement de test pour réellement injecter la valeur de l'URL, car Mockito injecte uniquement des faux.

12
Brice

J'avais une liste de chaînes lisant à partir du fichier Propriétés. La méthode setField de la classe ReflectionTestUtils utilisée dans le bloc @Before m'a aidé à définir ces valeurs avant l'exécution de mon test. Cela a fonctionné parfaitement même pour ma couche dao qui dépend de la classe Common DaoSupport.

@Before
public void setList() {
    List<String> mockedList = new ArrayList<>();
    mockedSimList.add("CMS");
    mockedSimList.add("SDP");
    ReflectionTestUtils.setField(mockedController, "ActualListInController",
            mockedList);
}
1
lakshmi

Vous ne devez pas vous moquer de la chose que vous essayez de tester. Cela ne sert à rien puisque vous ne touchez au code que vous essayez de tester. Récupérez plutôt l'instance de CustomerServiceImpl dans le contexte.

1
John B

Vous pouvez utiliser cette petite classe utilitaire ( Gist ) pour injecter automatiquement des valeurs de champ dans une classe cible:

public class ValueInjectionUtils {
  private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
  private static final ConversionService CONVERSION_SERVICE = new DefaultConversionService();
  private static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =
      new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, SystemPropertyUtils.PLACEHOLDER_SUFFIX,
          SystemPropertyUtils.VALUE_SEPARATOR, true);

  public static void injectFieldValues(Object testClassInstance, Properties properties) {
    for (Field field : FieldUtils.getFieldsListWithAnnotation(testClassInstance.getClass(), Value.class)) {
      String value = field.getAnnotation(Value.class).value();
      if (value != null) {
        try {
          Object resolvedValue = resolveValue(value, properties);
          FieldUtils.writeField(field, testClassInstance, CONVERSION_SERVICE.convert(resolvedValue, field.getType()),
              true);
        } catch (IllegalAccessException e) {
          throw new IllegalStateException(e);
        }
      }
    }
  }

  private static Object resolveValue(String value, Properties properties) {
    String replacedPlaceholderString = PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(value, properties);
    return evaluateSpEL(replacedPlaceholderString, properties);
  }

  private static Object evaluateSpEL(String value, Properties properties) {
    Expression expression = EXPRESSION_PARSER.parseExpression(value, new TemplateParserContext());
    EvaluationContext context =
        SimpleEvaluationContext.forPropertyAccessors(new MapAccessor()).withRootObject(properties).build();
    return expression.getValue(context);
  }
}

Il utilise org.Apache.commons.lang3.reflect.FieldUtils Pour accéder à tous les champs annotés avec @Value, Puis utilise les classes utilitaires Spring pour résoudre toutes les valeurs d'espace réservé. Vous pouvez également changer le type de paramètre properties en PlaceholderResolver au cas où vous voudriez utiliser votre propre PlaceholderResolver. Dans votre test, vous pouvez l'utiliser pour injecter un ensemble de valeurs données en tant qu'instance Map ou Properties comme dans l'exemple suivant:

HashMap<String, Object> props = new HashMap<>();
props.put("custom.url", "http://some.url");

Properties properties = new Properties();
properties.put("myProp", props);

ValueInjectionUtils.injectFieldValues(testTarget, properties);

Cela tentera ensuite de résoudre tous les champs annotés @Value Dans votre dataService. Personnellement, je préfère cette solution à ReflectionTestUtils.setField(dataService, "field", "value"); car vous n'avez pas à vous fier aux noms de champs codés en dur.

0
Jan Gassen