Population Spring @Value durante Unit Test


238

Sto cercando di scrivere un Unit Test per un semplice bean utilizzato nel mio programma per convalidare i moduli. Il bean è annotato con @Componente ha una variabile di classe inizializzata usando

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

Vorrei scrivere unit test per i metodi di validazione all'interno di questa classe, tuttavia, se possibile, vorrei farlo senza utilizzare il file delle proprietà. Il mio ragionamento dietro questo, è che se il valore che sto estraendo dal file delle proprietà cambia, vorrei che ciò non influisse sul mio caso di test. Il mio test case sta testando il codice che convalida il valore, non il valore stesso.

Esiste un modo per utilizzare il codice Java all'interno della mia classe di test per inizializzare una classe Java e popolare la proprietà Spring @Value all'interno di quella classe e quindi usarla per testare?

Ho trovato questo How To che sembra essere vicino, ma utilizza ancora un file delle proprietà. Preferirei che fosse tutto un codice Java.


Ho descritto una soluzione qui per un problema simile. Spero che sia d'aiuto.
horizon7,

Risposte:


199

Se possibile, proverei a scrivere quei test senza Spring Context. Se crei questa classe nel tuo test senza primavera, hai il pieno controllo sui suoi campi.

Per impostare il @valuecampo puoi usare Molle ReflectionTestUtils- ha un metodo setFieldper impostare campi privati.

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


2
Esattamente quello che stavo cercando di fare e quello che stavo cercando per impostare il valore all'interno della mia classe, grazie!
Kyle,

2
O anche senza dipendenze di Spring cambiando il campo in accesso predefinito (pacchetto protetto) per renderlo semplicemente accessibile al test.
Arne Burmeister,

22
Esempio:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Olivier

4
È possibile che si desideri impostare questi campi impostati dal costruttore e quindi spostare l' @Valueannotazione sul parametro del costruttore. Questo rende il codice di test molto più semplice quando si scrive il codice manualmente e Spring Boot non se ne cura.
Thorbjørn Ravn Andersen,

Questa è la risposta migliore per cambiare rapidamente una proprietà per un singolo testcase.
membri del

194

Dalla primavera 4.1 è possibile impostare i valori delle proprietà solo nel codice utilizzando org.springframework.test.context.TestPropertySource annotazione a livello di classe Test unitari. È possibile utilizzare questo approccio anche per iniettare proprietà in istanze di bean dipendenti

Per esempio

@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();
    }

  }

}

Nota: è necessario disporre dell'istanza diorg.springframework.context.support.PropertySourcesPlaceholderConfigurer nel contesto Spring

Modifica 24-08-2017: se si utilizza SpringBoot 1.4.0 e versioni successive, è possibile inizializzare i test con @SpringBootTeste le @SpringBootConfigurationannotazioni. Ulteriori informazioni qui

In caso di SpringBoot abbiamo il seguente codice

@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);
  }

}

3
Grazie, finalmente qualcuno ha risposto a come sovrascrivere Value e non come impostare un campo. Derivo i valori dal campo stringa in PostConstruct e quindi ho bisogno che il valore stringa sia impostato da Spring, non dopo la costruzione.
tequilacat,

@Value ("$ aaaa") - puoi usare questo Config all'interno della stessa classe?
Kalpesh Soni,

Non sono sicuro perché Config è una classe statica. Ma non esitate a controllare
Dmytro Boichenko,

Come posso usare l'annotazione @Value nella classe Mockito Test?
user1575601

Sto scrivendo un test di integrazione per un servizio che non fa riferimento a nessun codice che recupera i valori dal file delle proprietà ma la mia applicazione ha una classe di configurazione che sta recuperando i valori dal file delle proprietà. Quindi, quando eseguo il test, viene visualizzato l'errore del segnaposto non risolto, ad esempio "$ {spring.redis.port}"
legenda

63

Non abusare dei campi privati ​​ottenuti / impostati per riflessione

Usare la riflessione come è fatto in diverse risposte qui è qualcosa che potremmo evitare.
Porta un piccolo valore qui mentre presenta molteplici inconvenienti:

  • rileviamo problemi di riflessione solo in fase di esecuzione (es: campi non esistenti più)
  • Vogliamo l'incapsulamento ma non una classe opaca che nasconda le dipendenze che dovrebbero essere visibili e rendere la classe più opaca e meno verificabile.
  • incoraggia il cattivo design. Oggi dichiari a @Value String field. Domani puoi dichiarare 5o 10di loro in quella classe e potresti non essere nemmeno consapevole di diminuire il design della classe. Con un approccio più visibile per impostare questi campi (come il costruttore), ci penserai due volte prima di aggiungere tutti questi campi e probabilmente li incapsulerai in un'altra classe e li utilizzerai @ConfigurationProperties.

Rendi la tua classe testabile sia in unità che in integrazione

Per poter scrivere sia i test unitari semplici (cioè senza contenitore a molla funzionante) sia i test di integrazione per la classe dei componenti Spring, è necessario rendere questa classe utilizzabile con o senza Spring.
Eseguire un container in un unit test quando non è necessario è una cattiva pratica che rallenta le build locali: non lo vuoi.
Ho aggiunto questa risposta perché nessuna risposta qui sembra mostrare questa distinzione e quindi si basano sistematicamente su un container in esecuzione.

Quindi penso che dovresti spostare questa proprietà definita come interna alla classe:

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

in un parametro del costruttore che verrà iniettato da Spring:

@Component
public class Foo{   
    private String property;

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

    //...         
}

Esempio di unit test

Puoi istanziare Foosenza Spring e iniettare qualsiasi valore per propertygrazie al costruttore:

public class FooTest{

   Foo foo = new Foo("dummyValue");

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

Esempio di test di integrazione

È possibile iniettare la proprietà nel contesto con Spring Boot in questo modo semplice grazie propertiesall'attributo di @SpringBootTest :

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

   @Autowired
   Foo foo;

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

È possibile utilizzare come alternativa @TestPropertySourcema aggiunge un'ulteriore annotazione:

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

Con Spring (senza Spring Boot), dovrebbe essere un po 'più complicato, ma dato che non ho usato Spring senza Spring Boot da molto tempo non preferisco dire una cosa stupida.

Come nota a margine: se hai molti @Valuecampi da impostare, estrarli in una classe annotata @ConfigurationPropertiesè più rilevante perché non vogliamo un costruttore con troppi argomenti.


1
Bella risposta. Le migliori pratiche qui sono anche per i campi inizializzati dal costruttore final, ovveroprivate String final property
kugo2006

1
È bello che qualcuno l'abbia sottolineato. Per farlo funzionare solo con Spring, è necessario aggiungere la classe sotto test in @ContextConfiguration.
vimterd

53

Se lo desideri, puoi comunque eseguire i test all'interno di Spring Context e impostare le proprietà richieste all'interno della classe di configurazione Spring. Se usi JUnit, usa SpringJUnit4ClassRunner e definisci una classe di configurazione dedicata per i tuoi test in questo modo:

La classe in prova:

@Component
public SomeClass {

    @Autowired
    private SomeDependency someDependency;

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

La classe di 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() { ... }
}

E la classe di configurazione per questo 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);
    }
}

Detto questo, non consiglierei questo approccio, l'ho appena aggiunto qui come riferimento. Secondo me il modo migliore è usare il corridore Mockito. In quel caso non esegui affatto test all'interno di Spring, il che è molto più chiaro e più semplice.


4
Sono d'accordo che la maggior parte della logica dovrebbe essere testata con Mockito. Vorrei che ci fosse un modo migliore per testare la presenza e la correttezza delle annotazioni rispetto all'esecuzione dei test attraverso Spring.
Altair7852

29

Questo sembra funzionare, sebbene sia ancora un po 'prolisso (vorrei ancora qualcosa di più corto):

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

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

2
Penso che questa risposta sia più pulita in quanto è agnostica di primavera, funziona bene per diversi scenari, come quando devi usare runner di test personalizzati e non puoi semplicemente aggiungere l' @TestPropertyannotazione.
raspacorp,

Questo funziona solo per l'approccio del test di integrazione di Spring. Alcune risposte e commenti qui si inclinano verso un approccio Mockito, per il quale questo sicuramente non funziona (dal momento che non c'è nulla in Mockito che popolerà @Valuei messaggi, indipendentemente dal fatto che la proprietà corrispondente sia impostata o meno.
Sander Verhagen

5

L'aggiunta di PropertyPlaceholderConfigurer nella configurazione funziona per me.

@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;
    }
}

E in classe di prova

@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());
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.