การเติม Spring @Value ระหว่างการทดสอบหน่วย


238

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

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

ฉันต้องการเขียนการทดสอบหน่วยสำหรับวิธีการตรวจสอบภายในคลาสนี้อย่างไรก็ตามถ้าเป็นไปได้ฉันต้องการจะทำโดยไม่ต้องใช้ไฟล์คุณสมบัติ เหตุผลของฉันที่อยู่เบื้องหลังคือถ้าค่าที่ฉันดึงจากการเปลี่ยนแปลงไฟล์คุณสมบัติฉันต้องการที่จะไม่ส่งผลกระทบต่อกรณีทดสอบของฉัน กรณีทดสอบของฉันกำลังทดสอบรหัสที่ตรวจสอบความถูกต้องของค่าไม่ใช่ตัวของตัวเอง

มีวิธีใช้รหัส Java ในคลาสทดสอบของฉันเพื่อเริ่มต้นคลาส Java และเติมคุณสมบัติ Spring @Value ภายในคลาสนั้นแล้วใช้เพื่อทดสอบด้วยหรือไม่

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


ฉันได้อธิบายวิธีแก้ปัญหาที่นี่สำหรับปัญหาที่คล้ายกัน หวังว่ามันจะช่วย
horizon7 7

คำตอบ:


199

ถ้าเป็นไปได้ฉันจะพยายามเขียนการทดสอบเหล่านั้นโดยไม่มีบริบทของฤดูใบไม้ผลิ หากคุณสร้างคลาสนี้ในการทดสอบโดยไม่มีสปริงคุณก็สามารถควบคุมฟิลด์ได้อย่างเต็มที่

ในการตั้งค่า@valueฟิลด์คุณสามารถใช้สปริงReflectionTestUtils- มีวิธีsetFieldตั้งค่าฟิลด์ส่วนตัว

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


2
สิ่งที่ฉันพยายามทำและสิ่งที่ฉันกำลังมองหาเพื่อกำหนดค่าภายในชั้นเรียนของฉันขอบคุณ!
Kyle

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

22
ตัวอย่าง:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Olivier

4
คุณอาจต้องการทำให้ฟิลด์เหล่านี้ถูกกำหนดโดยนวกรรมิกแล้วย้าย@Valueบันทึกย่อไปยังพารามิเตอร์คอนสตรัค สิ่งนี้ทำให้รหัสทดสอบง่ายขึ้นมากเมื่อเขียนโค้ดด้วยตนเองและ Spring Boot ไม่สนใจ
Thorbjørn Ravn Andersen

นี่คือคำตอบที่ดีที่สุดในการเปลี่ยนสถานที่ให้บริการสำหรับ testcase เพียงชิ้นเดียวอย่างรวดเร็ว
membersound

194

ตั้งแต่ Spring 4.1 คุณสามารถตั้งค่าคุณสมบัติได้โดยใช้รหัส org.springframework.test.context.TestPropertySourceคำอธิบายประกอบในระดับการทดสอบหน่วย คุณสามารถใช้วิธีนี้แม้จะฉีดคุณสมบัติเข้าไปในอินสแตนซ์ของถั่วที่ขึ้นต่อกัน

ตัวอย่างเช่น

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

  }

}

หมายเหตุ:จำเป็นต้องมีอินสแตนซ์ของorg.springframework.context.support.PropertySourcesPlaceholderConfigurerในบริบทสปริง

แก้ไข 24-08-2017:หากคุณใช้ SpringBoot 1.4.0 และใหม่กว่าคุณสามารถเริ่มต้นการทดสอบด้วยคำอธิบายประกอบ@SpringBootTestและ @SpringBootConfigurationข้อมูลเพิ่มเติมที่นี่

ในกรณีของ SpringBoot เรามีรหัสต่อไปนี้

@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
ขอขอบคุณในที่สุดก็มีคนตอบวิธีการแทนที่ค่าและไม่ใช่วิธีตั้งค่าเขตข้อมูล ฉันได้รับค่าจากเขตข้อมูลสตริงใน PostConstruct ดังนั้นฉันจึงต้องการค่าสตริงที่จะตั้งค่าโดย Spring ไม่ใช่หลังการสร้าง
tequilacat

@Value ("$ aaaa") - คุณสามารถใช้สิ่งนี้ภายในคลาส Config ได้หรือไม่?
Kalpesh Soni

ฉันไม่แน่ใจเพราะ Config เป็นคลาสคงที่ แต่โปรดตรวจสอบ
Dmytro Boichenko

ฉันจะใช้คำอธิบายประกอบ @Value ในชั้นเรียนทดสอบ Mockito ได้อย่างไร
user1575601

ฉันกำลังเขียนการทดสอบการรวมสำหรับบริการที่ไม่อ้างอิงรหัสใด ๆ ที่ดึงค่าจากไฟล์คุณสมบัติ แต่แอปพลิเคชันของฉันมีคลาสการกำหนดค่าซึ่งดึงค่าจากไฟล์คุณสมบัติ ดังนั้นเมื่อฉันรันการทดสอบมันทำให้เกิดข้อผิดพลาดของตัวยึดตำแหน่งที่ไม่แน่นอนให้พูดว่า "$ {spring.redis.port}"
ตำนาน

63

อย่าใช้ช่องทางส่วนตัวในทางที่ผิดโดยได้รับ / สะท้อน

การใช้การไตร่ตรองอย่างที่ทำในหลายคำตอบที่นี่เป็นสิ่งที่เราสามารถหลีกเลี่ยงได้
มันนำค่าเล็กน้อยมาที่นี่ในขณะที่นำเสนอข้อบกพร่องหลายประการ:

  • เราตรวจพบปัญหาการสะท้อนที่รันไทม์เท่านั้น (เช่น: ฟิลด์ที่ไม่มีอยู่อีกต่อไป)
  • เราต้องการ encapsulation แต่ไม่ใช่คลาสทึบแสงที่ซ่อนการพึ่งพาที่ควรมองเห็นและทำให้คลาสทึบแสงมากขึ้นและทดสอบน้อยลง
  • มันส่งเสริมการออกแบบที่ไม่ดี @Value String fieldวันนี้คุณประกาศ พรุ่งนี้คุณสามารถประกาศ5หรือ10ของพวกเขาในชั้นเรียนนั้นและคุณอาจไม่รู้ตัวเลยว่าคุณลดการออกแบบชั้นเรียนลง ด้วยวิธีการที่ชัดเจนยิ่งขึ้นในการตั้งค่าเขตข้อมูลเหล่านี้ (เช่นตัวสร้าง) คุณจะคิดสองครั้งก่อนที่จะเพิ่มเขตข้อมูลเหล่านี้ทั้งหมดและคุณอาจจะแค็ปซูลเขตข้อมูลเหล่านั้นลงในคลาสและการใช้งาน@ConfigurationPropertiesอื่น

ทำให้ชั้นเรียนของคุณสามารถทดสอบได้ทั้งแบบรวมและรวมเข้าด้วยกัน

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

ดังนั้นฉันคิดว่าคุณควรย้ายคุณสมบัตินี้ถูกกำหนดเป็นภายในของคลาส:

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

เป็นพารามิเตอร์ตัวสร้างที่จะถูกฉีดโดย Spring:

@Component
public class Foo{   
    private String property;

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

    //...         
}

ตัวอย่างการทดสอบหน่วย

คุณสามารถสร้างอินสแตนซ์Fooโดยไม่มีสปริงและฉีดค่าใด ๆpropertyขอบคุณผู้สร้าง:

public class FooTest{

   Foo foo = new Foo("dummyValue");

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

ตัวอย่างการทดสอบการรวม

คุณสามารถฉีดคุณสมบัติในบริบทด้วย Spring Boot ด้วยวิธีง่ายๆนี้ด้วยpropertiesคุณสมบัติของ@SpringBootTest :

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

   @Autowired
   Foo foo;

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

คุณสามารถใช้เป็นทางเลือก @TestPropertySourceแต่เพิ่มหมายเหตุประกอบเพิ่มเติม:

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

สำหรับ Spring (ที่ไม่มี Spring Boot) มันควรจะซับซ้อนกว่านี้เล็กน้อย แต่เนื่องจากฉันไม่ได้ใช้ Spring โดยไม่ต้อง Spring Boot มานานฉันไม่ชอบพูดสิ่งที่โง่

ตามหมายเหตุด้านข้าง: หากคุณมีหลาย@Valueฟิลด์ที่จะตั้งค่าการแยกออกเป็นคลาสที่มีคำอธิบายประกอบ@ConfigurationPropertiesนั้นมีความเกี่ยวข้องมากกว่าเพราะเราไม่ต้องการตัวสร้างที่มีอาร์กิวเมนต์มากเกินไป


1
คำตอบที่ดี แนวปฏิบัติที่เหมาะสมที่สุดสำหรับเขตข้อมูลที่เป็นค่าเริ่มต้นของfinalprivate String final property
คอนสตรัคเตอร์

1
เป็นเรื่องดีที่มีคนไฮไลต์อยู่ เพื่อให้ทำงานกับ Spring เท่านั้นจำเป็นต้องเพิ่มคลาสภายใต้การทดสอบใน @ContextConfiguration
vimterd

53

หากคุณต้องการคุณยังสามารถเรียกใช้การทดสอบของคุณภายใน Spring Context และตั้งค่าคุณสมบัติที่ต้องการภายในคลาสการกำหนดค่าของ Spring หากคุณใช้ JUnit ให้ใช้ SpringJUnit4ClassRunner และกำหนดคลาสการกำหนดค่าเฉพาะสำหรับการทดสอบของคุณดังนี้:

ชั้นเรียนที่อยู่ภายใต้การทดสอบ:

@Component
public SomeClass {

    @Autowired
    private SomeDependency someDependency;

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

ชั้นทดสอบ:

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

และคลาสการกำหนดค่าสำหรับการทดสอบนี้:

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

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


4
ฉันยอมรับว่าตรรกะส่วนใหญ่ควรทดสอบกับ Mockito ฉันหวังว่าจะมีวิธีที่ดีกว่าในการทดสอบสถานะและความถูกต้องของคำอธิบายประกอบมากกว่าใช้การทดสอบผ่านสปริง
Altair7852

29

ดูเหมือนว่าจะใช้งานได้แม้ว่าจะยังค่อนข้างละเอียด (ฉันต้องการบางสิ่งบางอย่างที่สั้นกว่า):

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

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

2
ฉันคิดว่าคำตอบนี้สะอาดกว่าเนื่องจากเป็นผู้ไม่เชื่อเรื่องพระเจ้าในฤดูใบไม้ผลิทำงานได้ดีกับสถานการณ์ที่แตกต่างกันเช่นเมื่อคุณต้องใช้นักวิ่งทดสอบที่กำหนดเองและไม่สามารถเพิ่ม@TestPropertyคำอธิบายประกอบได้
raspacorp

ใช้งานได้กับวิธีการทดสอบการรวมระบบของสปริงเท่านั้น คำตอบและความคิดเห็นบางส่วนในที่นี้โน้มตัวไปทาง Mockito ซึ่งสิ่งนี้ไม่ได้ผล (เนื่องจากไม่มีสิ่งใดใน Mockito ที่จะเติมข้อมูล@Valueให้โดยไม่คำนึงว่าจะมีการตั้งค่าคุณสมบัติที่สอดคล้องกันหรือไม่
Sander Verhagen

5

การเพิ่ม PropertyPlaceholderConfigurer ในการกำหนดค่าใช้งานได้สำหรับฉัน

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

และในชั้นเรียนทดสอบ

@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());
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.