web-dev-qa-db-fra.com

Remplissage de Spring @Value pendant le test unitaire

J'essaie d'écrire un test unitaire pour un simple haricot utilisé dans mon programme pour valider les formulaires. Le bean est annoté avec @Component et possède une variable de classe initialisée à l'aide de @Value("${this.property.value}") private String thisProperty;.

J'aimerais écrire des tests unitaires pour les méthodes de validation dans cette classe. Toutefois, si possible, j'aimerais 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, j'aimerais que cela n'affecte pas mon cas de test. Mon cas de test teste le code qui valide la valeur, pas la valeur elle-même.

Existe-t-il un moyen d'utiliser le code Java dans ma classe de test pour initialiser une classe Java et renseigner la propriété Spring @Value dans cette classe, puis l'utiliser à des fins de test?

J'ai trouvé ceci How To qui semble être proche, mais utilise toujours un fichier de propriétés. Je préférerais que tout soit Java code.

Merci

191
Kyle

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

Pour définir le champ @value, vous pouvez utiliser Ressorts ReflectionTestUtils - il dispose d'une méthode setField pour définir des champs privés.

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

169
Ralph

Depuis Spring 4.1, vous pouvez définir des valeurs de propriété simplement dans le code en utilisant l'annotation _org.springframework.test.context.TestPropertySource_ au niveau de la classe de tests unitaires. Vous pouvez utiliser cette approche même pour l'injection de propriétés dans des instances de bean dépendant

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.PropertySourcesPlaceholderConfigurer_ dans un contexte Spring.

Edit le 24-08-2017: Si vous utilisez SpringBoot 1.4.0 ou une version ultérieure, vous pouvez initialiser les tests avec @SpringBootTest et @SpringBootConfiguration annotations. 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);
  }

}
_
148
Dmytro Boichenko

Si vous le souhaitez, vous pouvez toujours exécuter vos tests dans Spring Context 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 méthode la plus efficace consiste à utiliser le coureur Mockito. Dans ce cas, vous n'exécutez pas de tests dans Spring, ce qui est beaucoup plus clair et simple.

49
Lukasz Korzybski

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

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

// Optionally:
@AfterClass
public static void afterClass() {
    System.clearProperty("some.property");
}
27
john16384

Ne pas abuser des champs privés get/set par réflexion

En utilisant la réflexion comme cela est fait dans plusieurs réponses, voici quelque chose que nous pourrions éviter.
Il apporte une petite valeur ici tout en présentant de multiples inconvénients:

  • nous détectons les problèmes de réflexion uniquement au moment de l'exécution (ex: les champs n'existant plus)
  • Nous voulons une encapsulation, mais pas une classe opaque qui cache des dépendances qui devraient être visibles et rend la classe plus opaque et moins testable.
  • cela encourage la mauvaise conception. Aujourd'hui, vous déclarez un @Value String field. Demain, vous pourrez déclarer 5 ou 10 dans cette classe et vous risquez même de ne pas être directement conscient du fait que vous diminuez la conception de la classe. Avec une approche plus visible pour définir ces champs (tels que le constructeur), vous réfléchirez à deux fois avant d'ajouter tous ces champs. Vous les encapsulerez probablement dans une autre classe et utiliserez @ConfigurationProperties.

Rendre 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 de votre classe de composants Spring, vous devez rendre cette classe utilisable avec ou sans Spring.
Exécuter un conteneur dans un test unitaire lorsque cela n’est pas requis est une mauvaise pratique qui ralentit les générations locales: vous ne le souhaitez pas.
J'ai ajouté cette réponse car aucune réponse ne semble montrer cette distinction et ils s'appuient donc systématiquement sur un conteneur en cours d'exécution.

Donc, je pense 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 Foo sans Spring et injecter n'importe quelle valeur pour property grâ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'attribut properties de @SpringBootTest:

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

   @Autowired
   Foo foo;

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

Vous pouvez utiliser comme alternative @TestPropertySource mais il 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, je ne préfère pas dire une chose stupide.

Remarque: si vous avez beaucoup de champs @Value à définir, les extraire dans une classe annotée avec @ConfigurationProperties est plus pertinent car nous ne voulons pas d'un constructeur avec trop d'arguments.

26
davidxxx

Ajouter PropertyPlaceholderConfigurer à 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());
    }
}
4
fjkjava