ฉันจะล้อเลียนช่อง @Value แบบอัตโนมัติใน Spring ด้วย Mockito ได้อย่างไร


125

ฉันใช้ Spring 3.1.4 RELEASE และ Mockito 1.9.5 ในชั้นเรียน Spring ของฉันฉันมี:

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

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

// ...

จากการทดสอบ JUnit ของฉันซึ่งฉันได้ตั้งค่าไว้ดังนี้:

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

ฉันต้องการจำลองค่าสำหรับฟิลด์ "defaultUrl" ของฉัน โปรดทราบว่าฉันไม่ต้องการเยาะเย้ยค่าสำหรับช่องอื่น - ฉันต้องการคงค่าเหล่านั้นไว้ตามที่เป็นจริงเฉพาะฟิลด์ "defaultUrl" โปรดทราบว่าฉันไม่มีเมธอด "setter" ที่ชัดเจน (เช่นsetDefaultUrl) ในชั้นเรียนของฉันและฉันไม่ต้องการสร้างขึ้นเพื่อจุดประสงค์ในการทดสอบเท่านั้น

ด้วยสิ่งนี้ฉันจะเยาะเย้ยค่าของฟิลด์นั้นได้อย่างไร

คำตอบ:


145

คุณสามารถใช้เวทมนตร์ของ Spring ReflectionTestUtils.setFieldเพื่อหลีกเลี่ยงการแก้ไขใด ๆ กับโค้ดของคุณ

ดูบทช่วยสอนนี้เพื่อดูข้อมูลเพิ่มเติมแม้ว่าคุณอาจไม่จำเป็นต้องใช้เพราะวิธีนี้ใช้งานง่ายมาก

UPDATE

ตั้งแต่เปิดตัว Spring 4.2.RC1 ตอนนี้คุณสามารถตั้งค่าฟิลด์คงที่ได้โดยไม่ต้องจัดหาอินสแตนซ์ของคลาส ดูนี้เป็นส่วนหนึ่งของเอกสารและนี้กระทำการ


13
ในกรณีที่ลิงก์เสียให้ใช้ReflectionTestUtils.setField(bean, "fieldName", "value");ก่อนที่จะเรียกใช้beanวิธีการของคุณในระหว่างการทดสอบ
Michał Stochmal

2
ทางออกที่ดีสำหรับการเยาะเย้ยคุณสมบัติที่ดึงมาจากไฟล์คุณสมบัติ
Antony Sampath Kumar Reddy

@ MichałStochmalการทำเช่นนั้นจะเกิดขึ้นตั้งแต่ยื่นเป็น java.lang.IllegalStateException ส่วนตัว: ไม่สามารถเข้าถึงเมธอด: คลาส org.springframework.util.ReflectionUtils ไม่สามารถเข้าถึงสมาชิกของคลาส com.kaleidofin.app.service.impl.CVLKRAProvider ด้วยตัวดัดแปลง "" ที่ org.springframework.util.ReflectionUtils.handleReflectionException (ReflectionUtils.java:112) ที่ org.springframework.util.ReflectionUtils.setField (ReflectionUtils.java:655)
Akhil Surapuram

113

ตอนนี้เป็นครั้งที่สามแล้วที่ฉันเข้าร่วมโพสต์ SO นี้เพราะฉันมักจะลืมวิธีล้อเลียนช่อง @Value แม้ว่าคำตอบที่ยอมรับจะถูกต้อง แต่ฉันก็ต้องใช้เวลาพอสมควรเพื่อให้การเรียก "setField" ถูกต้องดังนั้นอย่างน้อยสำหรับตัวฉันเองฉันก็วางตัวอย่างข้อมูลที่นี่:

ชั้นการผลิต:

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

ชั้นทดสอบ:

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

คุณยังสามารถจำลองการกำหนดค่าคุณสมบัติของคุณในชั้นเรียนทดสอบของคุณ

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

26

ฉันต้องการแนะนำวิธีแก้ปัญหาที่เกี่ยวข้องซึ่งก็คือการส่ง@Valueฟิลด์ -annotated เป็นพารามิเตอร์ไปยังตัวสร้างแทนที่จะใช้ReflectionTestUtilsคลาส

แทนสิ่งนี้:

public class Foo {

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

และ

public class FooTest {

    @InjectMocks
    private Foo foo;

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

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

ทำเช่นนี้:

public class Foo {

    private String foo;

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

และ

public class FooTest {

    private Foo foo;

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

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

ประโยชน์ของแนวทางนี้: 1) เราสามารถสร้างอินสแตนซ์คลาส Foo ได้โดยไม่ต้องพึ่งคอนเทนเนอร์ (เป็นเพียงตัวสร้าง) และ 2) เราไม่ได้เชื่อมโยงการทดสอบกับรายละเอียดการใช้งานของเรา (การสะท้อนจะเชื่อมโยงเรากับชื่อฟิลด์โดยใช้สตริง ซึ่งอาจทำให้เกิดปัญหาหากเราเปลี่ยนชื่อฟิลด์)


3
ข้อเสีย: หากมีคนยุ่งกับคำอธิบายประกอบเช่นใช้คุณสมบัติ 'bar' แทน 'foo' การทดสอบของคุณจะยังคงใช้งานได้ ฉันเพิ่งมีกรณีนี้
Nils El-Himoud

@ NilsEl-Himoud นั่นเป็นประเด็นที่ยุติธรรมโดยทั่วไปสำหรับคำถาม OP แต่ปัญหาที่คุณยกขึ้นไม่ได้ดีไปกว่าหรือแย่ไปกว่าการใช้เครื่องมือสะท้อนกับตัวสร้าง ประเด็นของคำตอบนี้คือการพิจารณาตัวสร้างมากกว่าการสะท้อนยูทิล (คำตอบที่ยอมรับ) มาร์คขอบคุณสำหรับคำตอบฉันขอขอบคุณสำหรับความง่ายและความสะอาดของการปรับแต่งนี้
Marquee

22

คุณสามารถใช้คำอธิบายประกอบการทดสอบฤดูใบไม้ผลินี้

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

ดู org.springframework.test.context.TestPropertySource

ตัวอย่างเช่นนี่คือคลาสทดสอบ:

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

และนี่คือคลาสที่มีคุณสมบัติ:

@Component
public class MyClass {

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

14

ฉันใช้รหัสด้านล่างและได้ผลสำหรับฉัน:

@InjectMocks
private ClassABC classABC;

@Before
public void setUp() {
    ReflectionTestUtils.setField(classABC, "constantFromConfigFile", 3);
}

อ้างอิง: https://www.jeejava.com/mock-an-autowired-value-field-in-spring-with-junit-mockito/


1
เหมือนกันที่นี่ ... +1
อ่อนโยน

ฉันทำแบบเดียวกัน แต่มันก็ยังไม่สะท้อน
Shubhro Mukherjee

1

โปรดทราบว่าฉันไม่มีเมธอด "setter" ที่ชัดเจน (เช่น setDefaultUrl) ในคลาสของฉันและฉันไม่ต้องการสร้างขึ้นเพื่อจุดประสงค์ในการทดสอบเท่านั้น

วิธีหนึ่งในการแก้ไขปัญหานี้คือเปลี่ยนคลาสของคุณไปใช้Constructor Injectionซึ่งใช้สำหรับการทดสอบและการฉีดสปริง ไม่มีการไตร่ตรองอีกต่อไป :)

ดังนั้นคุณสามารถส่งสตริงใดก็ได้โดยใช้ตัวสร้าง:

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

}

และในการทดสอบของคุณให้ใช้มัน:

MySpringClass MySpringClass  = new MySpringClass("anyUrl", "anyPassword");
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.