Около года назад я написал в блоге, как издеваться над Spring Bean . Описанные там шаблоны были немного инвазивны для производственного кода. Как правильно @Profile один из читателей Колин в комментарии , есть лучшая альтернатива шпионскому / фиктивному Spring bean-компоненту на @Profile аннотации @Profile . Этот пост в блоге будет описывать эту технику. Я использовал этот подход с успехом на работе, а также в моих побочных проектах.
Обратите внимание, что широко распространенные насмешки в вашем приложении часто считаются запахом дизайна.
Представляем производственный код
Прежде всего нам нужен тестируемый код, чтобы продемонстрировать макетирование. Мы будем использовать эти простые классы:
|
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
|
@Repositorypublic class AddressDao { public String readAddress(String userName) { return "3 Dark Corner"; }}@Servicepublic class AddressService { private AddressDao addressDao; @Autowired public AddressService(AddressDao addressDao) { this.addressDao = addressDao; } public String getAddressForUser(String userName){ return addressDao.readAddress(userName); }}@Servicepublic 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); }} |
Конечно, этот код не имеет особого смысла, но будет хорошо продемонстрировать, как имитировать Spring bean. AddressDao просто возвращает строку и, таким образом, имитирует чтение из некоторого источника данных. Он автоматически подключен к AddressService . Этот bean-компонент автоматически подключается к UserService , который используется для создания строки с именем пользователя и адресом.
Обратите внимание, что мы используем инжекцию конструктора, так как инъекция поля считается плохой практикой. Если вы хотите применить внедрение конструктора для своего приложения, Оливер Джирке (разработчик экосистемы Spring и руководитель Data Data) недавно создал очень хороший проект Ninjector .
Конфигурация, которая сканирует все эти компоненты, является довольно стандартным основным классом Spring Boot:
|
1
2
3
4
5
6
|
@SpringBootApplicationpublic class SimpleApplication { public static void main(String[] args) { SpringApplication.run(SimpleApplication.class, args); }} |
Mock Spring bean (без AOP)
Давайте AddressService класс AddressService где мы AddressService . Мы можем создать этот макет с помощью аннотаций Spring ‘ @Profiles и @Primary следующим образом:
|
1
2
3
4
5
6
7
8
9
|
@Profile("AddressService-test")@Configurationpublic class AddressDaoTestConfiguration { @Bean @Primary public AddressDao addressDao() { return Mockito.mock(AddressDao.class); }} |
Эта тестовая конфигурация будет применяться, только если AddressService-test Spring AddressService-test . Когда он применяется, он регистрирует bean-компонент типа AddressDao , который является фиктивным экземпляром, созданным Mockito . @Primary аннотация говорит Spring использовать этот экземпляр вместо реального, когда кто-то автоматически связывает компонент AddressDao.
Тестовый класс использует фреймворк JUnit :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@ActiveProfiles("AddressService-test")@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(SimpleApplication.class)public class AddressServiceITest { @Autowired private AddressService addressService; @Autowired private AddressDao addressDao; @Test public void testGetAddressForUser() { // GIVEN Mockito.when(addressDao.readAddress("john")) .thenReturn("5 Bright Corner"); // WHEN String actualAddress = addressService.getAddressForUser("john"); // THEN Assert.assertEquals("5 Bright Corner", actualAddress); }} |
Мы активируем профиль AddressService-test чтобы включить AddressService-test . Аннотация @RunWith необходима для интеграционных тестов Spring, а @SpringApplicationConfiguration определяет, какая конфигурация Spring будет использоваться для построения контекста для тестирования. Перед тестом мы автоматически подключаем экземпляр AddressService и макет AddressService .
Последующий метод тестирования должен быть понятен, если вы используете Mockito. На этапе GIVEN мы записываем желаемое поведение в фиктивный экземпляр. На этапе WHEN мы выполняем тестовый код, а на этапе THEN мы проверяем, вернул ли тестируемый код ожидаемое нами значение.
Шпион на бобов весны (без АОП)
Для примера шпионажа, будем шпионить AddressService экземпляром AddressService :
|
1
2
3
4
5
6
7
8
9
|
@Profile("UserService-test")@Configurationpublic class AddressServiceTestConfiguration { @Bean @Primary public AddressService addressServiceSpy(AddressService addressService) { return Mockito.spy(addressService); }} |
Эта конфигурация Spring будет проверяться компонентом только в том случае, если будет активен профиль UserService-test . Он определяет основной компонент типа AddressService . @Primary указывает Spring использовать этот экземпляр в случае, если в контексте Spring присутствуют два bean-компонента этого типа. Во время создания этого компонента мы автоматически подключаем существующий экземпляр AddressService из контекста Spring и используем шпионскую функцию Mockito. Бин, который мы регистрируем, фактически делегирует все вызовы оригинальному экземпляру, но шпионаж Mockito позволяет нам проверять взаимодействия в шпионском экземпляре.
Мы протестируем поведение UserService следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@ActiveProfiles("UserService-test")@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(SimpleApplication.class)public class UserServiceITest { @Autowired private UserService userService; @Autowired private AddressService addressService; @Test public void testGetUserDetails() { // GIVEN - Spring scanned by SimpleApplication class // WHEN String actualUserDetails = userService.getUserDetails("john"); // THEN Assert.assertEquals("User john, 3 Dark Corner", actualUserDetails); Mockito.verify(addressService).getAddressForUser("john"); }} |
Для тестирования мы активируем UserService-test чтобы применить нашу конфигурацию шпионажа. Мы автоматически подключаем UserService который тестируется, и AddressService , который отслеживается через Mockito.
Нам не нужно готовить какое-либо поведение для тестирования в фазе GIVEN . Фаза W HEN явно выполняет тестируемый код. На THEN этапе мы проверяем, вернул ли addressService тестовый код ожидаемое значение, а также был ли addressService вызов addressService с правильным параметром.
Проблемы с Mockito и Spring AOP
Допустим, теперь мы хотим использовать модуль Spring AOP для решения некоторых сквозных задач. Например, чтобы регистрировать вызовы на наших бобах Spring следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
package net.lkrnac.blog.testing.mockbeanv2.aoptesting;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.context.annotation.Profile;import org.springframework.stereotype.Component;import lombok.extern.slf4j.Slf4j; @Aspect@Component@Slf4j@Profile("aop") //only for example purposespublic class AddressLogger { @Before("execution(* net.lkrnac.blog.testing.mockbeanv2.beans.*.*(..))") public void logAddressCall(JoinPoint jp){ log.info("Executing method {}", jp.getSignature()); }} |
Этот аспект AOP применяется перед вызовом bean-компонентов Spring из пакета net.lkrnac.blog.testing.mockbeanv2 . Он использует аннотацию Lombok @Slf4j для записи сигнатуры вызываемого метода. Обратите внимание, что этот компонент создается только тогда, когда aop профиль aop . Мы используем этот профиль для разделения примеров тестирования AOP и не AOP. В реальном приложении вы не захотите использовать такой профиль.
Нам также необходимо включить AspectJ для нашего приложения, поэтому все следующие примеры будут использовать этот основной класс Spring Boot:
|
1
2
3
4
5
6
7
|
@SpringBootApplication@EnableAspectJAutoProxypublic class AopApplication { public static void main(String[] args) { SpringApplication.run(AopApplication.class, args); }} |
Конструкции AOP включены с помощью @EnableAspectJAutoProxy .
Но такие конструкции АОП могут быть проблематичными, если мы объединяем Mockito для насмешки с Spring AOP. Это связано с тем, что оба используют CGLIB для прокси-серверов реальных экземпляров, и когда прокси-сервер Mockito помещается в Spring-прокси, мы можем столкнуться с проблемами несоответствия типов. Их можно уменьшить, настроив область действия bean-компонента с помощью ScopedProxyMode.TARGET_CLASS , но вызовы Mockito verify () прежнему не NotAMockException с NotAMockException . Такие проблемы можно увидеть, если мы aop профиль для UserServiceITest .
Макет боба Spring, представленный Spring AOP
Чтобы преодолеть эти проблемы, мы завернем макет в этот Spring bean:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
package net.lkrnac.blog.testing.mockbeanv2.aoptesting;import org.mockito.Mockito;import org.springframework.context.annotation.Primary;import org.springframework.context.annotation.Profile;import org.springframework.stereotype.Repository;import lombok.Getter;import net.lkrnac.blog.testing.mockbeanv2.beans.AddressDao;@Primary@Repository@Profile("AddressService-aop-mock-test")public class AddressDaoMock extends AddressDao{ @Getter private AddressDao mockDelegate = Mockito.mock(AddressDao.class); public String readAddress(String userName) { return mockDelegate.readAddress(userName); }} |
@Primary аннотация гарантирует, что этот бин будет иметь приоритет перед реальным @Primary во время внедрения. Чтобы убедиться, что он будет применяться только для конкретного теста, мы определяем профиль AddressService-aop-mock-test для этого компонента. Он наследует класс AddressDao , так что он может действовать как полная замена этого типа.
Чтобы имитировать поведение, мы определяем фиктивный экземпляр типа AddressDao , который предоставляется через геттер, определенный аннотацией Lombok @Getter . Мы также реализуем readAddress() который предполагается вызывать во время теста. Этот метод просто делегирует вызов ложному экземпляру.
Тест, в котором используется этот макет, может выглядеть так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@ActiveProfiles({"AddressService-aop-mock-test", "aop"})@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(AopApplication.class)public class AddressServiceAopMockITest { @Autowired private AddressService addressService; @Autowired private AddressDao addressDao; @Test public void testGetAddressForUser() { // GIVEN AddressDaoMock addressDaoMock = (AddressDaoMock) addressDao; Mockito.when(addressDaoMock.getMockDelegate().readAddress("john")) .thenReturn("5 Bright Corner"); // WHEN String actualAddress = addressService.getAddressForUser("john"); // THEN Assert.assertEquals("5 Bright Corner", actualAddress); }} |
В тесте мы определяем AddressService-aop-mock-test чтобы активировать aop профиль aop чтобы активировать аспект AddressLogger AOP. Для тестирования мы проводим автоматическое тестирование bean addressService и его поддельной зависимости addressDao . Как мы знаем, addressDao будет иметь тип AddressDaoMock , потому что этот компонент был помечен как @Primary . Поэтому мы можем привести его и записать поведение в mockDelegate .
Когда мы вызываем метод тестирования, следует использовать записанное поведение, потому что мы ожидаем, что метод тестирования будет использовать зависимость AddressDao .
Шпион на Spring bean прокси по Spring AOP
Подобный шаблон можно использовать для отслеживания реальной реализации. Вот как может выглядеть наш шпион:
|
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
|
package net.lkrnac.blog.testing.mockbeanv2.aoptesting;import org.mockito.Mockito;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Primary;import org.springframework.context.annotation.Profile;import org.springframework.stereotype.Service;import lombok.Getter;import net.lkrnac.blog.testing.mockbeanv2.beans.AddressDao;import net.lkrnac.blog.testing.mockbeanv2.beans.AddressService;@Primary@Service@Profile("UserService-aop-test")public class AddressServiceSpy extends AddressService{ @Getter private AddressService spyDelegate; @Autowired public AddressServiceSpy(AddressDao addressDao) { super(null); spyDelegate = Mockito.spy(new AddressService(addressDao)); } public String getAddressForUser(String userName){ return spyDelegate.getAddressForUser(userName); }} |
Как мы видим, этот шпион очень похож на AddressDaoMock . Но в этом случае реальный компонент использует инжекцию конструктора, чтобы автоматически связать свою зависимость. Поэтому нам нужно определить конструктор не по умолчанию и также выполнить инжекцию конструктора. Но мы не будем передавать введенную зависимость в родительский конструктор.
Чтобы включить слежку за реальным объектом, мы создаем новый экземпляр со всеми зависимостями, помещаем его в экземпляр шпионского Mockito и сохраняем его в spyDelegate . Мы ожидаем вызова метода getAddressForUser() во время теста, поэтому мы делегируем этот вызов spyDelegate . Это свойство может быть доступно в тесте через геттер, определенный аннотацией Lombok @Getter .
Сам тест выглядел бы так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@ActiveProfiles({"UserService-aop-test", "aop"})@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(AopApplication.class)public class UserServiceAopITest { @Autowired private UserService userService; @Autowired private AddressService addressService; @Test public void testGetUserDetails() { // GIVEN AddressServiceSpy addressServiceSpy = (AddressServiceSpy) addressService; // WHEN String actualUserDetails = userService.getUserDetails("john"); // THEN Assert.assertEquals("User john, 3 Dark Corner", actualUserDetails); Mockito.verify(addressServiceSpy.getSpyDelegate()).getAddressForUser("john"); }} |
Это очень прямо вперед. Профиль UserService-aop-test гарантирует, что AddressServiceSpy будет сканироваться. Профиль aop обеспечивает то же самое для аспекта AddressLogger . Когда мы автоматически проводим тестирование объекта UserService и его зависимости AddressService , мы знаем, что можем привести его к AddressServiceSpy и проверить вызов его свойства spyDelegate после вызова метода тестирования.
Поддельный Spring bean, представленный Spring AOP
Очевидно, что делегирование вызовов в шутки или шпионы Мокито усложняет тестирование. Эти шаблоны часто бывают лишними, если нам просто нужно подделать логику. Мы можем использовать такую подделку в таком случае:
|
1
2
3
4
5
6
7
8
|
@Primary@Repository@Profile("AddressService-aop-fake-test")public class AddressDaoFake extends AddressDao{ public String readAddress(String userName) { return userName + "'s address"; }} |
и использовал его для тестирования таким образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
@ActiveProfiles({"AddressService-aop-fake-test", "aop"})@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(AopApplication.class)public class AddressServiceAopFakeITest { @Autowired private AddressService addressService; @Test public void testGetAddressForUser() { // GIVEN - Spring context // WHEN String actualAddress = addressService.getAddressForUser("john"); // THEN Assert.assertEquals("john's address", actualAddress); }} |
Я не думаю, что этот тест требует объяснения.
| Ссылка: | Как издеваться над Spring bean (версия 2) от нашего партнера по JCG Любоса Крнака в |