【问题标题】:Populating Spring @Value during Unit Test在单元测试期间填充 Spring @Value
【发布时间】:2013-06-25 13:19:36
【问题描述】:

我正在尝试为我的程序中用于验证表单的简单 bean 编写单元测试。该 bean 用 @Component 注释,并有一个使用

初始化的类变量
@Value("${this.property.value}") private String thisProperty;

我想为这个类中的验证方法编写单元测试,但是,如果可能的话,我想在不使用属性文件的情况下这样做。我的理由是,如果我从属性文件中提取的值发生变化,我希望这不会影响我的测试用例。我的测试用例是测试验证值的代码,而不是值本身。

有没有办法在我的测试类中使用 Java 代码来初始化 Java 类并填充该类中的 Spring @Value 属性,然后使用它进行测试?

我确实发现这个How To 似乎很接近,但仍然使用属性文件。我宁愿都是 Java 代码。

【问题讨论】:

  • 我已经描述了类似问题的解决方案here。希望对您有所帮助。

标签: java spring junit spring-annotations


【解决方案1】:

如果可能的话,我会尝试在没有 Spring Context 的情况下编写这些测试。如果您在没有 spring 的测试中创建此类,那么您可以完全控制其字段。

要设置 @value 字段,您可以使用 Springs ReflectionTestUtils - 它有一个方法 setField 来设置私有字段。

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

【讨论】:

  • 或者甚至完全没有 Spring 依赖,通过将字段更改为默认访问(包受保护)以使其易于测试访问。
  • 示例:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
  • 您可能希望这些字段由构造函数设置,然后将@Value 注释移动到构造函数参数。这使得手动编写代码时测试代码变得更加简单,Spring Boot 不在乎。
  • 这是快速更改单个测试用例的一个属性的最佳答案。
【解决方案2】:

如果您愿意,您仍然可以在 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 中运行测试,这更加清晰和简单。

【讨论】:

  • 我同意大多数逻辑都应该使用 Mockito 进行测试。我希望有比通过 Spring 运行测试更好的方法来测试注释的存在性和正确性。
【解决方案3】:

这似乎可行,虽然仍然有点冗长(我想要更短的):

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

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

【讨论】:

  • 我认为这个答案更简洁,因为它与 Spring 无关,它适用于不同的场景,比如当您必须使用自定义测试运行器并且不能只添加 @TestProperty 注释时。
  • 这仅适用于 Spring 集成测试方法。这里的一些答案和 cmets 倾向于使用 Mockito 方法,这肯定行不通(因为 Mockito 中没有任何内容可以填充@Values,无论是否设置了相应的属性。
【解决方案4】:

在配置中添加 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());
    }
}

【讨论】:

    【解决方案5】:

    从 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 注释初始化测试。更多信息here

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

    【讨论】:

    • 谢谢,终于有人回答了如何覆盖值而不是如何设置字段。我从 PostConstruct 中的字符串字段派生值,因此我需要由 Spring 设置字符串值,而不是在构造之后。
    • @Value("$aaaa") - 你可以在类 Config 中使用它吗?
    • 我不确定,因为 Config 是静态类。但请随时检查
    • 如何在 Mockito 测试类中使用 @Value 注解?
    • 我正在为一项服务编写集成测试,该服务不引用任何从属性文件中获取值的代码,但我的应用程序具有从属性文件中获取值的配置类。所以当我运行测试时,它给出了 unresolve placeholder 的错误,比如 "${spring.redis.port}"
    【解决方案6】:

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

    我们可以避免在几个答案中使用反射。
    它在这里带来的价值很小,但也有很多缺点:

    • 我们仅在运行时检测反射问题(例如:不再存在的字段)
    • 我们想要封装,而不是一个不透明的类,它隐藏了应该可见的依赖项,并使该类更加不透明且更难测试。
    • 它鼓励糟糕的设计。今天你声明一个@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 注释的类中更为相关,因为我们不希望构造函数具有太多参数。

    【讨论】:

    • 很好的答案。这里的最佳实践也是构造函数初始化字段为final,即private String final property
    • 很高兴有人强调了这一点。为了使其仅适用于 Spring,有必要在 @ContextConfiguration 中添加被测类。
    • 如果我有 10 个字段,@Value 你建议创建一个有 10 个参数的构造函数?
    • @ACV 从不。请阅读我回答的最后一句话。如果要设置许多字段,则配置属性类可能具有设置器。对于仅设计为属性持有者的类来说,这不那么烦人。
    • 我认为这个答案为问题提供了真正的解决方案。
    【解决方案7】:

    Spring Boot 自动为我们做了很多事情,但是当我们使用注解 @SpringBootTest 时,我们认为一切都会被 Spring Boot 自动解决。

    有很多文档,但最少的是选择一个 engine (@RunWith(SpringRunner.class)) 并指明将用于创建 context 加载的类配置(resources/applicationl.properties)。

    您需要引擎上下文

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = MyClassTest .class)
    public class MyClassTest {
    
        @Value("${my.property}")
        private String myProperty;
    
        @Test
        public void checkMyProperty(){
            Assert.assertNotNull(my.property);
        }
    }
    

    当然,如果您查看 Spring Boot 文档,您会发现数以千计的操作系统方法可以做到这一点。

    【讨论】:

      【解决方案8】:

      这是个很老的问题,当时我不确定它是否是一个选项,但这就​​是为什么我总是更喜欢构造函数而不是值的 DependencyInjection。

      我可以想象你的班级可能是这样的:

      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 而自己创建所有对象。

      【讨论】:

        【解决方案9】:

        在 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}) 进行外部配置

        【讨论】:

          【解决方案10】:
          @ExtendWith(SpringExtension.class)    // @RunWith(SpringJUnit4ClassRunner.class)
          @ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class)
          

          可能会有所帮助。关键是 ConfigDataApplicationContextInitializer 获取所有 props 数据

          【讨论】:

            猜你喜欢
            • 2019-08-25
            • 2019-10-19
            • 1970-01-01
            • 2014-02-24
            • 1970-01-01
            • 2014-02-28
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多