Заполнение Spring @Value во время модульного теста

238

Я пытаюсь написать модульный тест для простого компонента, который используется в моей программе для проверки форм. Компонент аннотирован @Componentи имеет переменную класса, которая инициализируется с помощью

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

Я хотел бы написать модульные тесты для методов проверки внутри этого класса, однако, если это возможно, я бы хотел сделать это без использования файла свойств. Мой аргумент заключается в том, что если значение, которое я извлекаю из файла свойств, изменится, я бы хотел, чтобы это не повлияло на мой тестовый пример. Мой тестовый пример проверяет код, который проверяет значение, а не само значение.

Есть ли способ использовать код Java внутри моего тестового класса, чтобы инициализировать класс Java и заполнить свойство Spring @Value внутри этого класса, а затем использовать его для тестирования?

Я нашел это How To , кажется, близко, но все еще использует файл свойств. Я бы предпочел, чтобы это был код Java.

рукав моря
источник
Я описал решение здесь для аналогичной проблемы. Надеюсь, поможет.
horizon7

Ответы:

199

Если возможно, я бы попытался написать эти тесты без Spring Context. Если вы создадите этот класс в своем тесте без пружины, то у вас будет полный контроль над его полями.

Для установки @valueполя вы можете использовать пружины ReflectionTestUtils- у него есть метод setFieldдля установки приватных полей.

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

Ральф
источник
2
Именно то, что я пытался сделать, и то, что я искал, чтобы установить значение в своем классе, спасибо!
Кайл
2
Или даже без Spring-зависимостей, изменив поле на доступ по умолчанию (защищенный пакет), чтобы сделать его просто доступным для теста.
Арне Бурмейстер
22
Пример:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Оливье
4
Вы можете захотеть сделать эти поля установленными конструктором, а затем переместить @Valueаннотацию в параметр конструктора. Это значительно упрощает тестовый код при написании кода вручную, и Spring Boot это не волнует.
Турбьёрн Равн Андерсен
Это лучший ответ, чтобы просто быстро изменить одно свойство для одного теста.
membersound
194

Начиная с Spring 4.1, вы можете устанавливать значения свойств только в коде, используя org.springframework.test.context.TestPropertySourceаннотацию на уровне класса Unit Tests. Вы можете использовать этот подход даже для внедрения свойств в зависимые экземпляры 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();
    }

  }

}

Примечание: необходимо иметь экземпляр org.springframework.context.support.PropertySourcesPlaceholderConfigurerв контексте Spring

Edit 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
Спасибо, наконец-то кто-то ответил, как переопределить Value, а не как установить поле. Я получаю значения из строкового поля в PostConstruct, и поэтому мне нужно, чтобы строковое значение было установлено Spring, а не после построения.
tequilacat
@Value ("$ aaaa") - вы можете использовать это внутри самого класса Config?
Kalpesh Soni
Я не уверен, потому что Config является статическим классом. Но, пожалуйста, не стесняйтесь проверять
Дмитрий Бойченко
Как я могу использовать аннотацию @Value в классе Mockito Test?
user1575601
Я пишу интеграционный тест для службы, которая не ссылается на код, извлекающий значения из файла свойств, но у моего приложения есть класс конфигурации, который извлекает значение из файла свойств. Поэтому, когда я запускаю тест, он выдает ошибку неразрешенного заполнителя, скажем «$ {spring.redis.port}»
легенда
63

Не злоупотребляйте приватными полями.

Использование рефлексии, как это делается в нескольких ответах, - это то, чего мы могли бы избежать.
Это приносит небольшую ценность здесь, в то время как это имеет много недостатков:

  • мы обнаруживаем проблемы с отражением только во время выполнения (например, поля больше не существуют)
  • Мы хотим инкапсуляцию, но не непрозрачный класс, который скрывает зависимости, которые должны быть видны, и делает класс более непрозрачным и менее тестируемым.
  • это поощряет плохой дизайн. Сегодня вы объявляете @Value String field. Завтра вы можете объявить 5или 10о них в этом классе, и вы можете даже не знать, что вы уменьшаете дизайн класса. При более наглядном подходе к установке этих полей (например, конструктора) вы дважды подумаете, прежде чем добавлять все эти поля, и, вероятно, вы инкапсулируете их в другой класс и используете @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;
    }

    //...         
}

Пример юнит-теста

Вы можете создать экземпляр Fooбез Spring и ввести любое значение 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, более актуально, потому что мы не хотим конструктор с слишком большим количеством аргументов.

davidxxx
источник
1
Отличный ответ. Лучшая практика здесь также для инициализированных конструктором полей final, т.е.private String final property
kugo2006
1
Хорошо, что кто-то это подчеркнул. Чтобы он работал только с Spring, необходимо добавить тестируемый класс в @ContextConfiguration.
Вимтерд
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 Runner. В этом случае вы вообще не запускаете тесты внутри Spring, что намного проще и понятнее.

Лукаш Коржибски
источник
4
Я согласен, что большая часть логики должна быть проверена с Mockito. Хотелось бы, чтобы был лучший способ проверки наличия и правильности аннотаций, чем запуск тестов через Spring.
Altair7852
29

Это, кажется, работает, хотя все еще немного многословно (я хотел бы кое-что более короткое все еще):

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

// Optionally:
@AfterClass
public static void afterClass() {
    System.clearProperty("some.property");
}
john16384
источник
2
Я думаю, что этот ответ является более чистым, так как он не зависит от Spring, он хорошо работает для различных сценариев, например, когда вам нужно использовать пользовательские тестовые прогоны и нельзя просто добавить @TestPropertyаннотацию.
Распакорп
Это работает только для подхода тестирования интеграции Spring. Некоторые ответы и комментарии здесь склоняются к подходу Mockito, для которого это, безусловно, не работает (поскольку в Mockito нет ничего, что будет заполнять @Values, независимо от того, установлено соответствующее свойство или нет.
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());
    }
}
fjkjava
источник