Статьи

Как издеваться над Spring Bean без Springockito

Я работаю с весной несколько лет. Но я всегда был разочарован тем, насколько грязной может стать конфигурация XML. По мере появления различных аннотаций и возможностей настройки Java я начал получать удовольствие от программирования на Spring. Вот почему я настоятельно рекомендую использовать конфигурацию Java. На мой взгляд, конфигурация XML подходит только тогда, когда вам нужно визуализировать Spring Integration или Spring Batch. Надеемся, что Spring Tool Suite также сможет визуализировать конфигурации Java для этих платформ.

Одним из неприятных аспектов конфигурации XML является то, что он часто приводит к огромным файлам конфигурации XML. Поэтому разработчики часто создают конфигурацию тестового контекста для интеграционного тестирования. Но какова цель интеграционного тестирования, когда не тестируется производственная проводка? Такой интеграционный тест имеет очень мало значения. Поэтому я всегда пытался спроектировать свои производственные контексты тестируемым способом.

Я исключаю, что когда вы создаете новый проект / модуль, вы максимально избегаете настройки XML. Таким образом, с помощью конфигурации Java вы можете создать конфигурацию Spring для модуля / пакета и сканировать их в основном контексте (@Configuration также является кандидатом для сканирования компонентов). Таким образом, вы можете естественным образом создавать острова Весенние бобы. Эти острова могут быть легко проверены в изоляции.

Но я должен признать, что не всегда возможно протестировать производственную конфигурацию Java как есть. Редко вам нужно изменить поведение или шпионить за определенными бобами. Для этого есть библиотека под названием Springockito . Честно говоря, я не использовал его до сих пор, потому что я всегда стараюсь проектировать конфигурацию Spring, чтобы избежать надругательства. Глядя на темпы развития Springockito и количество открытых вопросов , я бы немного беспокоился о том, чтобы внедрить его в свой набор тестов. Тот факт, что последний выпуск был сделан до выпуска Spring 4, поднимает такие вопросы, как «Возможно ли легко интегрировать его с Spring 4?». Я не знаю, потому что я не пробовал это. Я предпочитаю чистый подход Spring, если мне нужно смоделировать Spring bean в интеграционном тесте.

Spring предоставляет аннотацию @Primary для указания того, какой компонент должен быть предпочтительным в случае, когда зарегистрированы два компонента с одинаковым типом. Это удобно, поскольку в интеграционном тесте вы можете переопределить производственный компонент с помощью поддельного компонента. Давайте рассмотрим этот подход и некоторые подводные камни на примерах.

Я выбрал эту упрощенную / фиктивную структуру производственного кода для демонстрации:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Repository
public class AddressDao {
    public String readAddress(String userName) {
        return "3 Dark Corner";
    }
}
 
@Service
public class AddressService {
    private AddressDao addressDao;
     
    @Autowired
    public AddressService(AddressDao addressDao) {
        this.addressDao = addressDao;
    }
     
    public String getAddressForUser(String userName){
        return addressDao.readAddress(userName);
    }
}
 
@Service
public class UserService {
    private AddressService addressService;
 
    @Autowired
    public UserService(AddressService addressService) {
        this.addressService = addressService;
    }
     
    public String getUserDetails(String userName){
        String address = addressService.getAddressForUser(userName);
        return String.format("User %s, %s", userName, address);
    }
}

Экземпляр одноэлементного компонента AddressService внедряется в AddressService . AddressService аналогичным образом используется в UserService .

Я должен предупредить вас на этом этапе. Мой подход немного инвазивен для производственного кода. Чтобы иметь возможность подделывать существующие производственные компоненты, мы должны зарегистрировать поддельные компоненты в интеграционном тесте. Но эти поддельные бины обычно находятся в том же поддереве пакета, что и производственные бины (при условии, что вы используете стандартную структуру файлов Maven: «src / main / java» и «src / test / java»). Поэтому, когда они находятся в одном поддереве пакета, они будут сканироваться во время интеграционных тестов. Но мы не хотим использовать все фальшивые бины во всех интеграционных тестах. Подделки могут сломать несвязанные интеграционные тесты. Таким образом, нам нужен механизм, позволяющий тесту использовать только определенные ложные бины. Это делается путем полного исключения поддельных компонентов из сканирования компонентов. Интеграционный тест явно определяет, какие подделки используются (покажет это позже). Теперь давайте рассмотрим механизм исключения поддельных бинов из компонентного сканирования. Мы определяем нашу собственную маркерную аннотацию:

1
2
public @interface BeanMock {
}

И исключить аннотацию @BeanMock из сканирования компонентов в основной конфигурации Spring.

1
2
3
4
5
@Configuration
@ComponentScan(excludeFilters = @Filter(BeanMock.class))
@EnableAutoConfiguration
public class Application {
}

Корневой пакет сканирования компонентов является текущим пакетом класса Application . Таким образом, все вышеупомянутые производственные бобы должны быть в одной или нескольких упаковках. Теперь нам нужно создать интеграционный тест для UserService . Давайте шпионим за адресным бином. Конечно, такое тестирование не имеет практического смысла с этим рабочим кодом, но это всего лишь пример. Итак, вот наш шпионский боб:

1
2
3
4
5
6
7
8
9
@Configuration
@BeanMock
public class AddressServiceSpy {
    @Bean
    @Primary
    public AddressService registerAddressServiceSpy(AddressService addressService) {
        return spy(addressService);
    }
}

AddressService компонент AddressService автоматически подключается из производственного контекста, помещается в шпион Mockito и регистрируется в качестве основного компонента для типа AddressService . @Primary аннотация гарантирует, что наш поддельный компонент будет использоваться в интеграционном тесте вместо рабочего компонента. Аннотация @BeanMock гарантирует, что этот компонент не может быть отсканирован сканированием компонента Application . Давайте посмотрим на интеграционный тест сейчас:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { Application.class, AddressServiceSpy.class })
public class UserServiceITest {
    @Autowired
    private UserService userService;
 
    @Autowired
    private AddressService addressService;
 
    @Test
    public void testGetUserDetails() {
        // GIVEN - spring context defined by Application class
 
        // WHEN
        String actualUserDetails = userService.getUserDetails("john");
 
        // THEN
        Assert.assertEquals("User john, 3 Dark Corner", actualUserDetails);
        verify(addressService, times(1)).getAddressForUser("john");
    }
}

Аннотация @SpringApplicationConfigration имеет два параметра. First ( Application.class ) объявляет тестируемую конфигурацию Spring. Второй параметр ( AddressServiceSpy.class ) указывает AddressServiceSpy.class компонент, который будет загружен для нашего тестирования в контейнер Spring IoC. Очевидно, что мы можем использовать столько бобовых подделок, сколько необходимо, но вы не хотите иметь много бобовых подделок. Этот подход следует использовать редко, и если вы наблюдаете, как часто используете подобные насмешки, у вас, вероятно, есть серьезные проблемы с жесткой связью в вашем приложении или в вашей группе разработчиков в целом. Методология TDD должна помочь вам решить эту проблему. Имейте в виду: «Меньше дразнить всегда лучше!». Поэтому рассмотрим изменения в дизайне производства, которые позволяют снизить использование макетов. Это касается и модульного тестирования.

В рамках интеграционного теста мы можем автоматически связать этот шпионский компонент и использовать его для различных проверок. В этом случае мы userService.getUserDetails метод тестирования userService.getUserDetails вызывал метод addressService.getAddressForUser с параметром «john».

У меня есть еще один пример. В этом случае мы не будем шпионить за производственным бобом. Мы будем издеваться над этим:

1
2
3
4
5
6
7
8
9
@Configuration
@BeanMock
public class AddressDaoMock {
    @Bean
    @Primary
    public AddressDao registerAddressDaoMock() {
        return mock(AddressDao.class);
    }
}

Мы снова переопределяем производственный компонент, но на этот раз мы заменяем его на макет Mockito . Мы можем записать поведение для макета в нашем интеграционном тесте:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { Application.class, AddressDaoMock.class })
public class AddressServiceITest {
    @Autowired
    private AddressService addressService;
 
    @Autowired
    private AddressDao addressDao;
 
    @Test
    public void testGetAddressForUser() {
        // GIVEN
        when(addressDao.readAddress("john")).thenReturn("5 Bright Corner");
 
        // WHEN
        String actualAddress = addressService.getAddressForUser("john");
 
        // THEN
        Assert.assertEquals("5 Bright Corner", actualAddress);
    }
 
    @After
    public void resetMock() {
        reset(addressDao);
    }
}

Мы загружаем @SpringApplicationConfiguration боб через параметр @SpringApplicationConfiguration . В тестовом методе мы используем метод addressDao.readAddress для возврата строки «5 Bright Corner», когда ему передается «john» в качестве параметра.

Но имейте в виду, что записанное поведение можно переносить в различные интеграционные тесты через контекст Spring. Мы не хотим, чтобы тесты влияли друг на друга. Таким образом, вы можете избежать будущих проблем в своем наборе тестов, сбрасывая макеты после теста. Это делается в методе resetMock .

Ссылка: Как издеваться над бобом Spring без Springockito от нашего партнера JCG, Любоса Крнаца, в