Comment se moquer d'un champ @Value auto-câblé dans Spring avec Mockito?

124

J'utilise Spring 3.1.4.RELEASE et Mockito 1.9.5. Dans ma classe de printemps, j'ai:

@Value("#{myProps['default.url']}")
private String defaultUrl;

@Value("#{myProps['default.password']}")
private String defaultrPassword;

// ...

De mon test JUnit, que j'ai actuellement mis en place comme ceci:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class MyTest 
{ 

Je voudrais moquer une valeur pour mon champ "defaultUrl". Notez que je ne veux pas simuler les valeurs des autres champs - j'aimerais les garder telles quelles, uniquement le champ "defaultUrl". Notez également que je n'ai pas de méthode "setter" explicite (par exemple setDefaultUrl) dans ma classe et que je ne veux pas en créer juste à des fins de test.

Compte tenu de cela, comment puis-je me moquer d'une valeur pour ce champ?

Dave
la source

Réponses:

145

Vous pouvez utiliser la magie de Spring's ReflectionTestUtils.setFieldafin d'éviter d'apporter des modifications à votre code.

Consultez ce tutoriel pour encore plus d'informations, même si vous n'en aurez probablement pas besoin car la méthode est très facile à utiliser

METTRE À JOUR

Depuis l'introduction de Spring 4.2.RC1, il est désormais possible de définir un champ statique sans avoir à fournir une instance de la classe. Voir cette partie de la documentation et ce commit.

geoand
la source
12
Juste en cas de lien mort: à utiliser ReflectionTestUtils.setField(bean, "fieldName", "value");avant d'appeler votre beanméthode pendant le test.
Michał Stochmal
2
Bonne solution pour se moquer des propriétés qui sont extraites du fichier de propriétés.
Antony Sampath Kumar Reddy
@ MichałStochmal, cela produira puisque le fichier est privé java.lang.IllegalStateException: Impossible d'accéder à la méthode: La classe org.springframework.util.ReflectionUtils ne peut pas accéder à un membre de la classe com.kaleidofin.app.service.impl.CVLKRAProvider avec des modificateurs "" à org.springframework.util.ReflectionUtils.handleReflectionException (ReflectionUtils.java:112) à org.springframework.util.ReflectionUtils.setField (ReflectionUtils.java:655)
Akhil Surapuram
113

C'était maintenant la troisième fois que je me regardais sur ce post SO car j'oublie toujours comment se moquer d'un champ @Value. Bien que la réponse acceptée soit correcte, j'ai toujours besoin de temps pour appeler correctement "setField", donc au moins pour moi, je colle un exemple d'extrait ici:

Classe de production:

@Value("#{myProps[‘some.default.url']}")
private String defaultUrl;

Classe de test:

import org.springframework.test.util.ReflectionTestUtils;

ReflectionTestUtils.setField(instanceUnderTest, "defaultUrl", "http://foo");
// Note: Don't use MyClassUnderTest.class, use the instance you are testing itself
// Note: Don't use the referenced string "#{myProps[‘some.default.url']}", 
//       but simply the FIELDs name ("defaultUrl")
BAERUS
la source
32

Vous pouvez également simuler la configuration de votre propriété dans votre classe de test

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class MyTest 
{ 
   @Configuration
   public static class MockConfig{
       @Bean
       public Properties myProps(){
             Properties properties = new Properties();
             properties.setProperty("default.url", "myUrl");
             properties.setProperty("property.value2", "value2");
             return properties;
        }
   }
   @Value("#{myProps['default.url']}")
   private String defaultUrl;

   @Test
   public void testValue(){
       Assert.assertEquals("myUrl", defaultUrl);
   }
}
Manuel Quinones
la source
25

Je voudrais suggérer une solution connexe, qui consiste à transmettre les @Valuechamps -annotés en tant que paramètres au constructeur, au lieu d'utiliser la ReflectionTestUtilsclasse.

Au lieu de cela:

public class Foo {

    @Value("${foo}")
    private String foo;
}

et

public class FooTest {

    @InjectMocks
    private Foo foo;

    @Before
    public void setUp() {
        ReflectionTestUtils.setField(Foo.class, "foo", "foo");
    }

    @Test
    public void testFoo() {
        // stuff
    }
}

Faites ceci:

public class Foo {

    private String foo;

    public Foo(@Value("${foo}") String foo) {
        this.foo = foo;
    }
}

et

public class FooTest {

    private Foo foo;

    @Before
    public void setUp() {
        foo = new Foo("foo");
    }

    @Test
    public void testFoo() {
        // stuff
    }
}

Avantages de cette approche: 1) nous pouvons instancier la classe Foo sans conteneur de dépendances (c'est juste un constructeur), et 2) nous ne couplons pas notre test à nos détails d'implémentation (la réflexion nous lie au nom du champ à l'aide d'une chaîne, ce qui pourrait poser un problème si nous changeons le nom du champ).

marque
la source
3
inconvénient: si quelqu'un gâche l'annotation, par exemple utilise une propriété «bar» au lieu de «foo», votre test fonctionnera toujours. J'ai juste ce cas.
Nils El-Himoud
@ NilsEl-Himoud C'est un bon point en général pour la question OP, mais le problème que vous soulevez n'est ni meilleur ni pire en utilisant des outils de réflexion vs constructeur. Le point de cette réponse était la considération du constructeur par rapport à la réflexion util (la réponse acceptée). Mark, merci pour la réponse, j'apprécie la facilité et la propreté de ce tweak.
Marquee
22

Vous pouvez utiliser cette annotation magique Spring Test:

@TestPropertySource(properties = { "my.spring.property=20" }) 

voir org.springframework.test.context.TestPropertySource

Par exemple, voici la classe de test:

@ContextConfiguration(classes = { MyTestClass.Config.class })
@TestPropertySource(properties = { "my.spring.property=20" })
public class MyTestClass {

  public static class Config {
    @Bean
    MyClass getMyClass() {
      return new MyClass ();
    }
  }

  @Resource
  private MyClass myClass ;

  @Test
  public void myTest() {
   ...

Et voici la classe avec la propriété:

@Component
public class MyClass {

  @Value("${my.spring.property}")
  private int mySpringProperty;
   ...
Thibault
la source
1

Notez également que je n'ai pas de méthodes "setter" explicites (par exemple setDefaultUrl) dans ma classe et que je ne veux pas en créer juste à des fins de test.

Une façon de résoudre ce problème consiste à modifier votre classe pour utiliser l' injection de constructeur , qui est utilisée pour les tests et l'injection de ressort. Plus de réflexion :)

Ainsi, vous pouvez passer n'importe quelle chaîne en utilisant le constructeur:

class MySpringClass {

    private final String defaultUrl;
    private final String defaultrPassword;

    public MySpringClass (
         @Value("#{myProps['default.url']}") String defaultUrl, 
         @Value("#{myProps['default.password']}") String defaultrPassword) {
        this.defaultUrl = defaultUrl;
        this.defaultrPassword= defaultrPassword;
    }

}

Et dans votre test, utilisez-le simplement:

MySpringClass MySpringClass  = new MySpringClass("anyUrl", "anyPassword");
Dherik
la source