在单元测试期间填充Spring @Value

381

我正试图为程序中用于验证表单的简单bean编写单元测试。该bean使用@Component进行注释,并具有通过以下方法初始化的类变量:

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

我想为此类中的验证方法编写单元测试,但如果可能的话,我希望在不使用属性文件的情况下进行。我的理由是,如果我从属性文件中提取的值更改了,我希望这不会影响我的测试用例。我的测试用例正在测试验证值的代码,而不是值本身。

是否有一种方法可以在我的测试类中使用Java代码来初始化一个Java类并填充该类内的Spring @Value属性,然后使用它进行测试?

我找到了这个指南,看起来很接近,但仍然使用属性文件。我更喜欢全部使用Java代码。


我在这里描述了一个类似问题的解决方案。希望能有所帮助。 - horizon7
13个回答

292

3
甚至可以完全不依赖于 Spring,只需将字段更改为默认访问(包保护),以便测试可以简单地访问它。 - Arne Burmeister
59
org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value"); - OlivierLarue
23
您可以通过构造函数设置这些字段,然后将@Value注解移至构造函数参数。这样,在手动编写代码时,测试代码会更加简单,而Spring Boot则不会有任何问题。 - Thorbjørn Ravn Andersen
这是最好的答案,可以快速更改单个测试用例的一个属性。 - membersound
ReflectionTestUtils适用于Java 17或更高版本吗?由于反射不再给出警告,而是直接报错。 - BugsOverflow
如果你想测试数据类型是否能够正确映射到@Value注解上,那么这样做是不够的。例如,double类型可能会出现问题。 - Viktor Reinok

260

从Spring 4.1开始,你可以在单元测试类级别上使用org.springframework.test.context.TestPropertySource注解来编写代码并设置属性值。即使是将属性注入到依赖的bean实例中,也可以使用这种方法。

例如:

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

  }

}

注意: 在Spring上下文中需要有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);
  }

}

6
谢谢,终于有人回答了如何覆盖值而不是如何设置字段的问题。我从PostConstruct中的字符串字段派生值,所以我需要Spring设置字符串值,而不是在构造之后设置。 - tequilacat
@Value("$aaaa") - 你可以在 Config 类内部使用它吗? - Kalpesh Soni
我不确定,因为Config是静态类。但请随意检查。 - Dmytro Boichenko
我正在为一个服务编写集成测试,该服务不引用从属性文件中获取值的任何代码,但我的应用程序具有配置类,该类正在从属性文件中获取值。因此,当我运行测试时,它会给出未解析占位符的错误,例如"${spring.redis.port}"。 - legend
您还可以使用application.properties文件来设置单元测试属性,如下所示:@TestPropertySource("/application.properties") - Valerij Dobler
显示剩余4条评论

173

不要滥用反射获取/设置私有字段

在这里使用反射是可以避免的。它虽然带来一定的价值,但也有多个缺点:

  • 只能在运行时检测到反射问题(例如:字段不再存在)
  • 我们需要封装,但不需要一个关于依赖性的不透明类,应该让依赖性可见,并且使得该类更加透明和易于测试。
  • 它鼓励不良设计。今天你声明一个@Value String field,明天你可能会在该类中声明510个这样的字段,而你甚至可能不知道这会降低该类的设计质量。通过更明显的方式来设置这些字段(例如构造函数),你将在添加所有这些字段之前三思而后行,并且可能将其封装成另一个类并使用@ConfigurationProperties

使您的类既可以进行单元测试又可以进行集成测试

为了能够编写纯粹的单元测试(即在没有运行Spring容器的情况下),以及针对您的Spring组件类的集成测试,您必须使此类可与或无需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;
    }

    //...         
}

单元测试示例

您可以在不使用Spring的情况下实例化Foo,并且由于构造函数,可以为property注入任何值:

public class FooTest{

   Foo foo = new Foo("dummyValue");

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

集成测试示例

借助@SpringBootTestproperties属性,您可以以这种简单的方式在Spring Boot中将属性注入上下文:

@SpringBootTest(properties="property.value=dummyValue")
public class FooTest{
    
   @Autowired
   Foo foo;
     
   @Test
   public void doThat(){
       ...
   }    
}

你可以使用 @TestPropertySource 作为替代方案,但它会增加一个额外的注释:

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

使用 Spring(不使用 Spring Boot)可能会更加复杂,但是由于我很久没有使用过没有 Spring Boot 的 Spring,所以我不想说一些愚蠢的话。

顺便说一句:如果您有许多需要设置的 @Value 字段,则将它们提取到一个带有 @ConfigurationProperties 注释的类中更为相关,因为我们不希望构造函数有太多参数。


5
很好的回答。在这里最佳实践也是将构造函数初始化的字段设置为final,即private String final property - kugo2006
2
很好有人提醒了。为了让它只与Spring一起工作,需要将测试类添加到@ContextConfiguration中。 - vimterd
1
如果我有10个带有@Value的字段,您建议创建一个具有10个参数的构造函数吗? - ACV
4
@ACV永远不会。请阅读我回答的最后一句话。如果需要设置许多属性,配置属性类可以具有setter方法。 对于仅旨在成为属性容器的类来说,这样做会更少烦人。 - davidxxx
1
我认为这个答案提供了真正的解决方案。 - Daniel Pop

58

如果你愿意,你仍然可以在Spring上下文中运行你的测试,并在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 runner。在这种情况下,您根本不需要在Spring内运行测试,这更加清晰简单。


4
我同意大部分逻辑应该使用Mockito进行测试。我希望有一种比通过Spring运行测试来测试注释存在性和正确性的更好的方式。 - Altair7852

34

这似乎可以工作,尽管还有点啰嗦(我仍希望更短些):

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

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

2
我认为这个答案更干净,因为它与Spring无关,适用于不同的场景,比如当你必须使用自定义测试运行器而不能只添加@TestProperty注释时。 - raspacorp
3
这仅适用于Spring集成测试方法。这里的一些答案和评论倾向于Mockito方法,但这肯定行不通(因为在Mockito中没有任何东西可以填充@Value,无论相应的属性是否设置)。 - Sander Verhagen

9
@ExtendWith(SpringExtension.class)    // @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class)

希望这可以帮到你。关键是要使用ConfigDataApplicationContextInitializer来获取所有的属性数据。


是的!太棒了!正是我所需要的,速度也非常快!(好吧...至少对于一个与spring-boot相关的测试来说是这样的)这应该是被接受的答案。 - Dylan Watson

6

这是一个相当旧的问题,我不确定在那个时候是否有这个选项,但这就是为什么我总是更喜欢通过构造函数而不是值来进行依赖注入的原因。

我可以想象你的类可能长成这样:

class ExampleClass{

   @Autowired
   private Dog dog;

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

   ...other stuff...
}

你可以将它更改为:
class ExampleClass{

   private Dog dog;
   private String thisProperty;

   //optionally @Autowire
   public ExampleClass(final Dog dog, @Value("${this.property.value}") final String thisProperty){
      this.dog = dog;
      this.thisProperty = thisProperty;
   }

   ...other stuff...
}

有了这个实现,Spring框架就能自动识别要注入的内容,但是在单元测试中,你可以按照需要进行操作。例如使用Spring自动装配每个依赖项,并手动通过构造函数注入它们来创建“ExampleClass”实例,或者只使用带有测试属性文件的Spring,或者根本不使用Spring并自己创建所有对象。


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

2
我使用了下面的代码,它对我起作用了:
@InjectMocks
private ClassNotify classNotify;

@BeforeEach
  void init() {
    closeable = MockitoAnnotations.openMocks(this);
    ReflectionTestUtils.setField(classNotify, "EventType", "test-event");

  }

1
你尝试过这种方法吗? - Siddharth Aadarsh

1
在springboot 2.4.1中,我只是在我的测试中添加了注释@SpringBootTest,显然,在src/test/resources/application.yml中设置了spring.profiles.active = test
我使用@ExtendWith({SpringExtension.class})@ContextConfiguration(classes = {RabbitMQ.class, GenericMapToObject.class, ModelMapper.class, StringUtils.class})来处理外部配置。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接