Remplir Spring @Value pendant le test unitaire

238

J'essaie d'écrire un test unitaire pour un simple bean utilisé dans mon programme pour valider les formulaires. Le bean est annoté avec @Componentet a une variable de classe qui est initialisée en utilisant

@Value("${this.property.value}") private String thisProperty;

Je voudrais écrire des tests unitaires pour les méthodes de validation à l'intérieur de cette classe, cependant, si possible, je voudrais le faire sans utiliser le fichier de propriétés. Mon raisonnement derrière cela est que si la valeur que je tire du fichier de propriétés change, je voudrais que cela n'affecte pas mon cas de test. Mon scénario de test teste le code qui valide la valeur, pas la valeur elle-même.

Existe-t-il un moyen d'utiliser du code Java dans ma classe de test pour initialiser une classe Java et remplir la propriété Spring @Value à l'intérieur de cette classe, puis l'utiliser pour tester avec?

J'ai trouvé ce mode d' emploi qui semble proche, mais utilise toujours un fichier de propriétés. Je préfère que tout soit du code Java.

Kyle
la source
J'ai décrit une solution ici pour un problème similaire. J'espère que ça aide.
horizon7

Réponses:

199

Si possible, j'essaierais d'écrire ces tests sans contexte Spring. Si vous créez cette classe dans votre test sans ressort, alors vous avez un contrôle total sur ses domaines.

Pour définir le @valuechamp, vous pouvez utiliser Springs ReflectionTestUtils- il a une méthode setFieldpour définir les champs privés.

@see JavaDoc: ReflectionTestUtils.setField (java.lang.Object, java.lang.String, java.lang.Object)

Ralph
la source
2
Exactement ce que j'essayais de faire et ce que je cherchais pour définir la valeur dans ma classe, merci!
Kyle
2
Ou même sans dépendances Spring du tout en changeant le champ en accès par défaut (package protégé) pour le rendre simplement accessible au test.
Arne Burmeister
22
Exemple:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Olivier
4
Vous souhaiterez peut-être définir ces champs par le constructeur, puis déplacer l' @Valueannotation vers le paramètre constructeur. Cela rend le code de test beaucoup plus simple lors de l'écriture manuelle du code, et Spring Boot s'en fiche.
Thorbjørn Ravn Andersen
C'est la meilleure réponse pour changer rapidement une propriété pour un seul testcase.
membersound
194

Depuis Spring 4.1, vous pouvez définir des valeurs de propriété uniquement dans le code en utilisant des org.springframework.test.context.TestPropertySourceannotations au niveau de la classe Tests unitaires. Vous pouvez utiliser cette approche même pour injecter des propriétés dans des instances de bean dépendantes

Par exemple

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = FooTest.Config.class)
@TestPropertySource(properties = {
    "some.bar.value=testValue",
})
public class FooTest {

  @Value("${some.bar.value}")
  String bar;

  @Test
  public void testValueSetup() {
    assertEquals("testValue", bar);
  }


  @Configuration
  static class Config {

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertiesResolver() {
        return new PropertySourcesPlaceholderConfigurer();
    }

  }

}

Remarque: il est nécessaire d'avoir une instance de org.springframework.context.support.PropertySourcesPlaceholderConfigurerdans le contexte Spring

Edit 24-08-2017: Si vous utilisez SpringBoot 1.4.0 et versions ultérieures, vous pouvez initialiser les tests avec @SpringBootTestet les @SpringBootConfigurationannotations. Plus d'infos ici

Dans le cas de SpringBoot, nous avons le code suivant

@SpringBootTest
@SpringBootConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource(properties = {
    "some.bar.value=testValue",
})
public class FooTest {

  @Value("${some.bar.value}")
  String bar;

  @Test
  public void testValueSetup() {
    assertEquals("testValue", bar);
  }

}
Dmytro Boichenko
la source
3
Merci, enfin quelqu'un a répondu comment remplacer la valeur et non comment définir un champ. Je dérive des valeurs du champ de chaîne dans PostConstruct et j'ai donc besoin que la valeur de chaîne soit définie par Spring, pas après la construction.
tequilacat
@Value ("$ aaaa") - pouvez-vous utiliser cette configuration à l'intérieur de la classe elle-même?
Kalpesh Soni
Je ne suis pas sûr car Config est une classe statique. Mais n'hésitez pas à vérifier
Dmytro Boichenko
Comment puis-je utiliser l'annotation @Value dans la classe Mockito Test?
user1575601
J'écris un test d'intégration pour un service qui ne fait référence à aucun code qui récupère les valeurs du fichier de propriétés mais mon application a une classe de configuration qui récupère la valeur du fichier de propriétés. Donc, quand je lance le test, cela donne une erreur d'espace réservé non résolu, dites "$ {spring.redis.port}"
légende
63

Ne pas abuser des champs privés obtenir / définir par réflexion

L'utilisation de la réflexion comme cela se fait dans plusieurs réponses est quelque chose que nous pourrions éviter.
Il apporte une petite valeur ici alors qu'il présente de multiples inconvénients:

  • nous ne détectons les problèmes de réflexion qu'au moment de l'exécution (ex: champs qui n'existent plus)
  • Nous voulons une encapsulation mais pas une classe opaque qui cache les dépendances qui devraient être visibles et rendre la classe plus opaque et moins testable.
  • il encourage une mauvaise conception. Aujourd'hui, vous déclarez a @Value String field. Demain, vous pouvez déclarer 5ou en 10faire partie dans cette classe et vous ne savez peut-être même pas que vous diminuez la conception de la classe. Avec une approche plus visible pour définir ces champs (comme le constructeur), vous y réfléchirez à deux fois avant d'ajouter tous ces champs et vous les encapsulerez probablement dans une autre classe et les utiliserez @ConfigurationProperties.

Rendez votre classe testable à la fois unitaire et en intégration

Pour pouvoir écrire à la fois des tests unitaires simples (c'est-à-dire sans conteneur Spring en cours d'exécution) et des tests d'intégration pour votre classe de composants Spring, vous devez rendre cette classe utilisable avec ou sans Spring.
L'exécution d'un conteneur dans un test unitaire lorsqu'il n'est pas requis est une mauvaise pratique qui ralentit les builds locaux: vous ne le voulez pas.
J'ai ajouté cette réponse car aucune réponse ici ne semble montrer cette distinction et ils s'appuient donc systématiquement sur un conteneur en cours d'exécution.

Je pense donc que vous devriez déplacer cette propriété définie comme un interne de la classe:

@Component
public class Foo{   
    @Value("${property.value}") private String property;
    //...
}

dans un paramètre constructeur qui sera injecté par Spring:

@Component
public class Foo{   
    private String property;

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

    //...         
}

Exemple de test unitaire

Vous pouvez instancier Foosans Spring et injecter n'importe quelle valeur propertygrâce au constructeur:

public class FooTest{

   Foo foo = new Foo("dummyValue");

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

Exemple de test d'intégration

Vous pouvez injecter la propriété dans le contexte avec Spring Boot de cette manière simple grâce à l' propertiesattribut @SpringBootTest :

@SpringBootTest(properties="property.value=dummyValue")
public class FooTest{

   @Autowired
   Foo foo;

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

Vous pouvez utiliser comme alternative @TestPropertySourcemais cela ajoute une annotation supplémentaire:

@SpringBootTest
@TestPropertySource("property.value=dummyValue")
public class FooTest{ ...}

Avec Spring (sans Spring Boot), cela devrait être un peu plus compliqué, mais comme je n'ai pas utilisé Spring sans Spring Boot depuis longtemps, je ne préfère pas dire une chose stupide.

En remarque: si vous avez plusieurs @Valuechamps à définir, les extraire dans une classe annotée avec @ConfigurationPropertiesest plus pertinent car nous ne voulons pas d'un constructeur avec trop d'arguments.

davidxxx
la source
1
Très bonne réponse. La meilleure pratique ici est également pour les champs initialisés par le constructeur final, c'estprivate String final property
kugo2006
1
C'est bien que quelqu'un ait souligné cela. Pour le faire fonctionner avec Spring uniquement, il est nécessaire d'ajouter la classe testée dans @ContextConfiguration.
vimterd
53

Si vous le souhaitez, vous pouvez toujours exécuter vos tests dans le contexte Spring et définir les propriétés requises dans la classe de configuration Spring. Si vous utilisez JUnit, utilisez SpringJUnit4ClassRunner et définissez une classe de configuration dédiée pour vos tests comme celui-ci:

La classe sous test:

@Component
public SomeClass {

    @Autowired
    private SomeDependency someDependency;

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

La classe de test:

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration(classes = SomeClassTestsConfig.class)
public class SomeClassTests {

    @Autowired
    private SomeClass someClass;

    @Autowired
    private SomeDependency someDependency;

    @Before
    public void setup() {
       Mockito.reset(someDependency);

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

Et la classe de configuration pour ce test:

@Configuration
public class SomeClassTestsConfig {

    @Bean
    public static PropertySourcesPlaceholderConfigurer properties() throws Exception {
        final PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
        Properties properties = new Properties();

        properties.setProperty("someProperty", "testValue");

        pspc.setProperties(properties);
        return pspc;
    }
    @Bean
    public SomeClass getSomeClass() {
        return new SomeClass();
    }

    @Bean
    public SomeDependency getSomeDependency() {
        // Mockito used here for mocking dependency
        return Mockito.mock(SomeDependency.class);
    }
}

Cela dit, je ne recommanderais pas cette approche, je l'ai simplement ajoutée ici pour référence. À mon avis, la meilleure façon est d'utiliser Mockito runner. Dans ce cas, vous n'exécutez pas du tout de tests dans Spring, ce qui est beaucoup plus clair et plus simple.

Lukasz Korzybski
la source
4
Je suis d'accord que la plupart des logiques devraient être testées avec Mockito. Je souhaite qu'il y ait un meilleur moyen de tester la présence et l'exactitude des annotations que d'exécuter des tests via Spring.
Altair7852
29

Cela semble fonctionner, bien que toujours un peu verbeux (j'aimerais encore quelque chose de plus court):

@BeforeClass
public static void beforeClass() {
    System.setProperty("some.property", "<value>");
}

// Optionally:
@AfterClass
public static void afterClass() {
    System.clearProperty("some.property");
}
john16384
la source
2
Je pense que cette réponse est plus propre car elle est indépendante de Spring, elle fonctionne bien pour différents scénarios, comme lorsque vous devez utiliser des lanceurs de test personnalisés et ne pouvez pas simplement ajouter l' @TestPropertyannotation.
raspacorp
Cela ne fonctionne que pour l'approche de test d'intégration Spring. Certaines réponses et commentaires ici penchent vers une approche Mockito, pour laquelle cela ne fonctionne certainement pas (car il n'y a rien dans Mockito qui remplira le @Values, que la propriété correspondante soit définie ou non.
Sander Verhagen
5

L'ajout de PropertyPlaceholderConfigurer dans la configuration fonctionne pour moi.

@Configuration
@ComponentScan
@EnableJpaRepositories
@EnableTransactionManagement
public class TestConfiguration {
    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        builder.setType(EmbeddedDatabaseType.DERBY);
        return builder.build();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setDataSource(dataSource());
        entityManagerFactoryBean.setPackagesToScan(new String[] { "com.test.model" });
        // Use hibernate
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
        entityManagerFactoryBean.setJpaProperties(getHibernateProperties());
        return entityManagerFactoryBean;
    }

    private Properties getHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.show_sql", "false");
        properties.put("hibernate.dialect", "org.hibernate.dialect.DerbyDialect");
        properties.put("hibernate.hbm2ddl.auto", "update");
        return properties;
    }

    @Bean
    public JpaTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
         transactionManager.setEntityManagerFactory(
              entityManagerFactory().getObject()
         );

         return transactionManager;
    }

    @Bean
    PropertyPlaceholderConfigurer propConfig() {
        PropertyPlaceholderConfigurer placeholderConfigurer = new PropertyPlaceholderConfigurer();
        placeholderConfigurer.setLocation(new ClassPathResource("application_test.properties"));
        return placeholderConfigurer;
    }
}

Et en classe de test

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestConfiguration.class)
public class DataServiceTest {

    @Autowired
    private DataService dataService;

    @Autowired
    private DataRepository dataRepository;

    @Value("${Api.url}")
    private String baseUrl;

    @Test
    public void testUpdateData() {
        List<Data> datas = (List<Data>) dataRepository.findAll();
        assertTrue(datas.isEmpty());
        dataService.updateDatas();
        datas = (List<Data>) dataRepository.findAll();
        assertFalse(datas.isEmpty());
    }
}
fjkjava
la source