Populating Spring @Value selama Unit Test

238

Saya mencoba menulis Tes Unit untuk kacang sederhana yang digunakan dalam program saya untuk memvalidasi formulir. Kacang tersebut dijelaskan dengan @Componentdan memiliki variabel kelas yang diinisialisasi menggunakan

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

Saya ingin menulis unit test untuk metode validasi di dalam kelas ini, namun, jika mungkin saya ingin melakukannya tanpa menggunakan file properti. Alasan saya di balik ini, adalah bahwa jika nilai yang saya tarik dari file properti berubah, saya ingin itu tidak mempengaruhi kasus pengujian saya. Kasing uji saya menguji kode yang memvalidasi nilai, bukan nilai itu sendiri.

Apakah ada cara untuk menggunakan kode Java di dalam kelas pengujian saya untuk menginisialisasi kelas Java dan mengisi properti Spring @Value di dalam kelas itu kemudian menggunakannya untuk mengujinya?

Saya memang menemukan Cara ini yang tampaknya dekat, tetapi masih menggunakan file properti. Saya lebih suka semuanya menjadi kode Java.

Kyle
sumber
Saya telah menjelaskan solusi di sini untuk masalah serupa. Semoga ini bisa membantu.
horizon7

Jawaban:

199

Jika mungkin saya akan mencoba menulis tes tersebut tanpa Konteks Musim Semi. Jika Anda membuat kelas ini dalam pengujian Anda tanpa pegas, maka Anda memiliki kontrol penuh atas bidangnya.

Untuk mengatur @valuebidang Anda dapat menggunakan Mata Air ReflectionTestUtils- ia memiliki metode setFielduntuk mengatur bidang pribadi.

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

Muntah
sumber
2
Persis apa yang saya coba lakukan dan apa yang saya cari untuk menetapkan nilai di dalam kelas saya, terima kasih!
Kyle
2
Atau bahkan tanpa dependensi Spring sama sekali dengan mengubah bidang ke akses default (paket dilindungi) agar mudah diakses ke tes.
Arne Burmeister
22
Contoh:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Olivier
4
Anda mungkin ingin membuat bidang-bidang ini disetel oleh konstruktor dan kemudian memindahkan @Valueanotasi ke parameter konstruktor. Ini membuat kode uji lebih sederhana saat menulis kode secara manual, dan Spring Boot tidak peduli.
Thorbjørn Ravn Andersen
Ini adalah jawaban terbaik untuk hanya dengan cepat mengubah satu properti untuk satu testcase.
membersound
194

Sejak Spring 4.1 Anda bisa mengatur nilai properti hanya dalam kode dengan menggunakan org.springframework.test.context.TestPropertySourceanotasi pada tingkat kelas Tes Unit. Anda bisa menggunakan pendekatan ini bahkan untuk menyuntikkan properti ke instance kacang dependen

Sebagai contoh

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

  }

}

Catatan: Ini perlu untuk memiliki instance org.springframework.context.support.PropertySourcesPlaceholderConfigurerdalam konteks Spring

Sunting 24-08-2017: Jika Anda menggunakan SpringBoot 1.4.0 dan yang lebih baru, Anda dapat menginisialisasi pengujian @SpringBootTestdan @SpringBootConfigurationanotasi. Info lebih lanjut di sini

Dalam hal SpringBoot kami memiliki kode berikut

@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
sumber
3
Terima kasih, akhirnya seseorang menjawab cara mengganti Nilai dan bukan cara mengatur bidang. Saya mendapatkan nilai dari bidang string di PostConstruct dan jadi saya perlu nilai string yang ditetapkan oleh Spring, bukan setelah konstruksi.
tequilacat
@Nilai ("$ aaaa") - dapatkah Anda menggunakan ini di dalam kelas Config sendiri?
Kalpesh Soni
Saya tidak yakin karena Config adalah kelas statis. Tapi jangan ragu untuk memeriksa
Dmytro Boichenko
Bagaimana saya bisa menggunakan anotasi @Nilai di kelas Tes Mockito?
user1575601
Saya menulis tes integrasi untuk layanan yang tidak merujuk kode apa pun yang mengambil nilai dari file properti tetapi aplikasi saya memiliki kelas konfigurasi yang mengambil nilai dari file properti. Jadi ketika saya menjalankan tes, ini memberikan kesalahan placeholder yang belum terselesaikan, katakan "$ {spring.redis.port}"
legenda
63

Jangan menyalahgunakan bidang pribadi dapatkan / ditetapkan oleh refleksi

Menggunakan refleksi seperti yang dilakukan dalam beberapa jawaban di sini adalah sesuatu yang bisa kita hindari.
Ini membawa nilai kecil di sini sementara itu menyajikan beberapa kelemahan:

  • kami mendeteksi masalah refleksi hanya saat runtime (mis: bidang tidak ada lagi)
  • Kami ingin enkapsulasi tetapi bukan kelas buram yang menyembunyikan dependensi yang harus terlihat dan membuat kelas lebih buram dan kurang teruji.
  • itu mendorong desain yang buruk. Hari ini Anda mendeklarasikan a @Value String field. Besok Anda dapat mendeklarasikan 5atau 10tentang mereka di kelas itu dan Anda bahkan mungkin tidak langsung sadar bahwa Anda mengurangi desain kelas tersebut. Dengan pendekatan yang lebih terlihat untuk mengatur bidang-bidang ini (seperti konstruktor), Anda akan berpikir dua kali sebelum menambahkan semua bidang ini dan Anda mungkin akan merangkumnya ke dalam kelas lain dan digunakan @ConfigurationProperties.

Jadikan kelas Anda dapat diuji baik secara kesatuan maupun dalam integrasi

Untuk dapat menulis kedua tes unit biasa (yang tanpa wadah pegas berjalan) dan tes integrasi untuk kelas komponen Pegas Anda, Anda harus membuat kelas ini dapat digunakan dengan atau tanpa Pegas.
Menjalankan wadah dalam unit test ketika tidak diperlukan adalah praktik buruk yang memperlambat pembangunan lokal: Anda tidak menginginkannya.
Saya menambahkan jawaban ini karena sepertinya tidak ada jawaban di sini yang menunjukkan perbedaan ini dan karena itu mereka mengandalkan wadah yang berjalan secara sistematis.

Jadi saya pikir Anda harus memindahkan properti ini didefinisikan sebagai internal kelas:

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

ke dalam parameter konstruktor yang akan disuntikkan oleh Spring:

@Component
public class Foo{   
    private String property;

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

    //...         
}

Contoh uji unit

Anda dapat instantiate Footanpa Spring dan menyuntikkan nilai apa pun untuk propertyterima kasih kepada konstruktor:

public class FooTest{

   Foo foo = new Foo("dummyValue");

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

Contoh uji integrasi

Anda dapat menyuntikkan properti dalam konteks dengan Spring Boot dengan cara sederhana ini berkat propertiesatribut @SpringBootTest :

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

   @Autowired
   Foo foo;

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

Anda dapat menggunakan sebagai alternatif @TestPropertySourcetetapi menambahkan anotasi tambahan:

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

Dengan Spring (tanpa Spring Boot), itu seharusnya menjadi sedikit lebih rumit tetapi karena saya tidak menggunakan Spring tanpa Spring Boot sejak lama, saya tidak suka mengatakan hal yang bodoh.

Sebagai catatan: jika Anda memiliki banyak @Valuebidang untuk diset, mengekstraknya ke dalam kelas yang diberi penjelasan @ConfigurationPropertieslebih relevan karena kami tidak ingin konstruktor dengan terlalu banyak argumen.

davidxxx
sumber
1
Jawaban yang bagus Praktik terbaik di sini juga untuk bidang yang diinisialisasi konstruktor final, yaituprivate String final property
kugo2006
1
Sangat menyenangkan bahwa seseorang menyoroti itu. Untuk membuatnya bekerja dengan Spring saja, perlu menambahkan kelas yang diuji di @ContextConfiguration.
vimterd
53

Jika mau, Anda masih dapat menjalankan tes dalam Konteks Musim Semi dan mengatur properti yang diperlukan di dalam kelas konfigurasi Spring. Jika Anda menggunakan JUnit, gunakan SpringJUnit4ClassRunner dan tentukan kelas konfigurasi khusus untuk pengujian Anda seperti itu:

Kelas yang diuji:

@Component
public SomeClass {

    @Autowired
    private SomeDependency someDependency;

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

Kelas tes:

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

Dan kelas konfigurasi untuk tes ini:

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

Karena itu, saya tidak akan merekomendasikan pendekatan ini, saya hanya menambahkannya di sini untuk referensi. Menurut saya cara yang jauh lebih baik adalah menggunakan pelari Mockito. Dalam hal ini Anda tidak menjalankan tes di dalam Spring sama sekali, yang jauh lebih jelas dan sederhana.

Lukasz Korzybski
sumber
4
Saya setuju bahwa sebagian besar logika harus diuji dengan Mockito. Saya berharap ada cara yang lebih baik untuk menguji keberadaan dan kebenaran anotasi daripada menjalankan tes melalui Spring.
Altair7852
29

Ini sepertinya berhasil, meskipun masih sedikit bertele-tele (saya masih menginginkan sesuatu yang lebih pendek):

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

// Optionally:
@AfterClass
public static void afterClass() {
    System.clearProperty("some.property");
}
john16384
sumber
2
Saya pikir jawaban ini lebih bersih karena Spring agnostik, ini bekerja dengan baik untuk skenario yang berbeda, seperti ketika Anda harus menggunakan pelari uji kustom dan tidak bisa hanya menambahkan @TestPropertyanotasi.
raspacorp
Ini hanya bekerja untuk pendekatan uji integrasi Spring. Beberapa jawaban dan komentar di sini condong ke arah pendekatan Mockito, yang tentu saja ini tidak berhasil (karena tidak ada apa pun di Mockito yang akan mengisi populasi @Value, terlepas dari apakah properti yang bersangkutan telah diatur atau tidak.
Sander Verhagen
5

Menambahkan PropertyPlaceholderConfigurer dalam konfigurasi berfungsi untuk saya.

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

Dan di kelas tes

@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
sumber