Около года назад я написал в блоге, как издеваться над 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
|
@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); } } |
Конечно, этот код не имеет особого смысла, но будет хорошо продемонстрировать, как имитировать Spring bean. AddressDao
просто возвращает строку и, таким образом, имитирует чтение из некоторого источника данных. Он автоматически подключен к AddressService
. Этот bean-компонент автоматически подключается к UserService
, который используется для создания строки с именем пользователя и адресом.
Обратите внимание, что мы используем инжекцию конструктора, так как инъекция поля считается плохой практикой. Если вы хотите применить внедрение конструктора для своего приложения, Оливер Джирке (разработчик экосистемы Spring и руководитель Data Data) недавно создал очень хороший проект Ninjector .
Конфигурация, которая сканирует все эти компоненты, является довольно стандартным основным классом Spring Boot:
1
2
3
4
5
6
|
@SpringBootApplication public 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" ) @Configuration public 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" ) @Configuration public 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 purposes public 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 @EnableAspectJAutoProxy public 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 Любоса Крнака в |