Населення весни @ Value під час тестування одиниць


238

Я намагаюся написати блок-тест для простого квасолі, який використовується в моїй програмі для перевірки форм. Бін анотований @Componentі має змінну класу, яка ініціалізується за допомогою

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

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

Чи є спосіб використовувати код Java всередині мого тестового класу для ініціалізації класу Java та заповнення властивості Spring @Value всередині цього класу, а потім використовувати його для тестування?

Я знайшов це, як це, здається, близько, але все ще використовує файл властивостей. Я вважаю за краще, щоб це все було кодом Java.


Я описав тут рішення подібної проблеми. Сподіваюся, це допомагає.
горизонт7

Відповіді:


199

Якщо можливо, я б спробував написати ці тести без Весняного контексту. Якщо ви створили цей клас у своєму тесті без весни, то ви маєте повний контроль над його полями.

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

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


2
Саме те, що я намагався зробити, і що я шукав, щоб встановити значення всередині свого класу, дякую!
Кайл

2
Або навіть без весняних залежностей взагалі, змінивши поле на доступ за замовчуванням (пакет захищений), щоб зробити його просто доступним для тесту.
Arne Burmeister

22
Приклад:org.springframework.test.util.ReflectionTestUtils.setField(classUnderTest, "field", "value");
Олів'є

4
Ви можете зробити ці поля, встановлені конструктором, а потім перемістити @Valueанотацію до параметра конструктора. Це робить тестовий код набагато простішим при написанні коду вручну, і Spring Boot не хвилює.
Thorbjørn Ravn Andersen

Це найкраща відповідь, щоб просто швидко змінити одну властивість для однієї проби.
membersound

194

З весни 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у контексті Spring

Редагувати 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?
Калпеш Соні

Я не впевнений, оскільки Config - це статичний клас. Але будь ласка сміливо перевіряйте
Дмитро Бойченко

Як я можу використовувати анотацію @Value в класі Mockito Test?
користувач1575601

Я пишу тест на інтеграцію для служби, яка не посилає жодного коду, який отримує значення з файлу властивостей, але мій додаток має клас конфігурації, який отримує значення з файлу властивостей. Тож коли я запускаю тест, він дає помилку нерозв’язування заповнювача, скажіть "$ {spring.redis.port}"
легенда

63

Не зловживайте приватними полями, отриманими / заданими за допомогою відображення

Використання рефлексії, як це робиться в декількох відповідях, тут є щось, чого ми могли б уникнути.
Тут він приносить невелику цінність, в той час як представляє багато недоліків:

  • ми виявляємо проблеми з відображенням лише під час виконання (наприклад: поля вже не існують)
  • Ми хочемо інкапсуляції, але не непрозорого класу, який приховує залежності, які повинні бути видимими і роблять клас більш непрозорим і менш випробуваним.
  • це заохочує поганий дизайн. Сьогодні ви оголошуєте @Value String field. Завтра ви можете оголосити 5або 10про них у тому класі, і ви навіть не можете зрозуміти, що ви зменшуєте дизайн класу. З більш наочним підходом до встановлення цих полів (наприклад, конструктора), ви подумайте двічі перед тим, як додати всі ці поля, і, ймовірно, інкапсулюйте їх в інший клас та використаєте @ConfigurationProperties.

Зробіть свій клас перевіряючим як унітарним, так і інтеграційним

Щоб мати змогу писати як прості тестові одиниці (тобто без запущеного пружинного контейнера), так і тести інтеграції для вашого компонентного компонента 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є більш релевантним, оскільки ми не хочемо конструктора з занадто великою кількістю аргументів.


1
Чудова відповідь. Найкращою практикою тут є також поля, ініціалізовані конструктором final, тобтоprivate String final property
kugo2006,

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

2
Я думаю, що ця відповідь є більш чистою, оскільки це весняний агностик, вона добре працює для різних сценаріїв, наприклад, коли вам доведеться використовувати власні тестові бігуни і не можна просто додати @TestPropertyанотацію.
raspacorp

Це працює лише для тестового підходу до інтеграції весни. Деякі відповіді та коментарі тут схиляються до 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());
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.