Статьи

Как издеваться над бобом Spring (версия 2)

Около года назад я написал в блоге, как издеваться над 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 Любоса Крнака в