Bagaimana cara memalsukan bidang @Value autowired di Spring dengan Mockito?


124

Saya menggunakan Spring 3.1.4.RELEASE dan Mockito 1.9.5. Di kelas Musim Semi saya, saya memiliki:

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

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

// ...

Dari pengujian JUnit saya, yang saat ini saya siapkan seperti ini:

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

Saya ingin mengejek nilai untuk bidang "defaultUrl" saya. Perhatikan bahwa saya tidak ingin mengejek nilai untuk bidang lain - Saya ingin mempertahankannya sebagaimana adanya, hanya bidang "defaultUrl". Juga perhatikan bahwa saya tidak memiliki metode "penyetel" yang eksplisit (misalnya setDefaultUrl) di kelas saya dan saya tidak ingin membuatnya hanya untuk tujuan pengujian.

Mengingat ini, bagaimana saya dapat membuat tiruan nilai untuk satu bidang itu?

Jawaban:


145

Anda dapat menggunakan keajaiban Spring ReflectionTestUtils.setFielduntuk menghindari modifikasi apa pun pada kode Anda.

Lihat tutorial ini untuk informasi lebih lanjut, meskipun Anda mungkin tidak memerlukannya karena metode ini sangat mudah digunakan

MEMPERBARUI

Sejak pengenalan Spring 4.2.RC1 sekarang dimungkinkan untuk menyetel bidang statis tanpa harus menyediakan turunan kelas. Lihat ini bagian dari dokumentasi dan ini komit.


12
Untuk berjaga-jaga jika tautan mati: gunakan ReflectionTestUtils.setField(bean, "fieldName", "value");sebelum menjalankan beanmetode Anda selama pengujian.
Michał Stochmal

2
Solusi bagus untuk mengejek properti yang diambil dari file properti.
Antony Sampath Kumar Reddy

@ MichałStochmal, melakukan itu akan menghasilkan karena diajukan adalah java.lang.IllegalStateException pribadi: Tidak dapat mengakses metode: Kelas org.springframework.util.ReflectionUtils tidak dapat mengakses anggota kelas com.kaleidofin.app.service.impl.CVLKRAProvider dengan pengubah "" di org.springframework.util.ReflectionUtils.handleReflectionException (ReflectionUtils.java:112) di org.springframework.util.ReflectionUtils.setField (ReflectionUtils.java:655)
Akhil Surapuram

113

Sekarang sudah ketiga kalinya saya googling sendiri ke posting SO ini karena saya selalu lupa cara mengejek bidang @Value. Meskipun jawaban yang diterima benar, saya selalu membutuhkan waktu untuk mendapatkan panggilan "setField" dengan benar, jadi setidaknya untuk diri saya sendiri, saya menempelkan cuplikan contoh di sini:

Kelas produksi:

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

Kelas tes:

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")

32

Anda juga dapat meniru konfigurasi properti Anda ke dalam kelas pengujian Anda

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

25

Saya ingin menyarankan solusi terkait, yaitu meneruskan @Valuebidang -annotated sebagai parameter ke konstruktor, daripada menggunakan ReflectionTestUtilskelas.

Daripada ini:

public class Foo {

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

dan

public class FooTest {

    @InjectMocks
    private Foo foo;

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

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

Melakukan hal ini:

public class Foo {

    private String foo;

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

dan

public class FooTest {

    private Foo foo;

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

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

Manfaat dari pendekatan ini: 1) kita dapat membuat instance kelas Foo tanpa wadah ketergantungan (ini hanya konstruktor), dan 2) kita tidak menggabungkan pengujian kita dengan detail implementasi kita (refleksi mengikat kita ke nama bidang menggunakan string, yang dapat menyebabkan masalah jika kita mengubah nama field).


3
downside: Jika seseorang mengacaukan anotasi, misalnya menggunakan properti 'bar' daripada 'foo', pengujian Anda akan tetap berfungsi. Saya hanya punya kasus ini.
Nils El-Himoud

@ NilsEl-Himoud Itu adalah poin yang adil secara umum untuk pertanyaan OP, tetapi masalah yang Anda angkat tidak lebih baik atau lebih buruk menggunakan utilitas refleksi vs konstruktor. Inti dari jawaban ini adalah pertimbangan konstruktor atas refleksi util (jawaban yang diterima). Mark, terima kasih atas jawabannya, saya menghargai kemudahan dan kebersihan tweak ini.
Marquee

22

Anda dapat menggunakan anotasi Tes Musim Semi ajaib ini:

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

lihat org.springframework.test.context.TestPropertySource

Misalnya, ini adalah kelas tesnya:

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

Dan ini kelas dengan properti:

@Component
public class MyClass {

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


1

Juga perhatikan bahwa saya tidak memiliki metode "penyetel" eksplisit (misalnya setDefaultUrl) di kelas saya dan saya tidak ingin membuatnya hanya untuk tujuan pengujian.

Salah satu cara untuk mengatasinya adalah dengan mengubah kelas Anda menggunakan Constructor Injection , yang digunakan untuk pengujian dan injeksi Spring. Tidak ada lagi refleksi :)

Jadi, Anda dapat meneruskan String apa pun menggunakan konstruktor:

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

}

Dan dalam pengujian Anda, gunakan saja:

MySpringClass MySpringClass  = new MySpringClass("anyUrl", "anyPassword");
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.