Одна из основных функций Spring — функция публикации событий. Мы можем использовать события для разделения частей нашего приложения и реализации шаблона публикации-подписки. Одна часть нашего приложения может публиковать событие, на которое реагируют несколько слушателей (даже асинхронно). В рамках Spring Framework 5.3.3 (Spring Boot 2.4.2) теперь мы можем записывать и проверять все опубликованные события ( ApplicationEvent
) при тестировании приложений Spring Boot с использованием @RecrodApplicationEvents
.
Настройка для записи ApplicationEvent с помощью Spring Boot
Чтобы использовать эту функцию, нам нужен только Spring Boot Starter Test, который является частью каждого проекта Spring Boot, который вы загружаете на start.spring.io .
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
Обязательно используйте версию Spring Boot >= 2.4.2, так как нам нужна версия Spring Framework >= 5.3.3.
Для наших тестов есть одно дополнительное требование: нам нужно работать со SpringTestContext
поскольку публикация событий является основной функциональностью платформы ApplicationContext
.
Следовательно, она не работает для модульного теста, где не используется поддержка инфраструктуры Spring TestContext. Есть несколько аннотаций тестовых срезов Spring Boot, которые удобно загружают контекст для нашего теста.
Введение в публикацию событий Spring
В качестве примера мы протестируем класс Java, который выдает UserCreationEvent,
когда мы успешно создаем нового пользователя. Событие включает метаданные о пользователе, актуальные для последующих задач:
public class UserCreationEvent extends ApplicationEvent { private final String username; private final Long id; public UserCreationEvent(Object source, String username, Long id) { super(source); this.username = username; this.id = id; } // getters }
Начиная со Spring Framework 4.2, нам не нужно расширять абстрактный класс ApplicationEvent
и мы можем использовать любой POJO в качестве нашего класса событий. В следующий статье привелено отличное введение в события приложений с помощью Spring Boot.
Наш UserService
создает и хранит наших новых пользователей. Мы можем создать как одного пользователя, так и группу пользователей:
@Service public class UserService { private final ApplicationEventPublisher eventPublisher; public UserService(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public Long createUser(String username) { // logic to create a user and store it in a database Long primaryKey = ThreadLocalRandom.current().nextLong(1, 1000); this.eventPublisher.publishEvent(new UserCreationEvent(this, username, primaryKey)); return primaryKey; } public List<Long> createUser(List<String> usernames) { List<Long> resultIds = new ArrayList<>(); for (String username : usernames) { resultIds.add(createUser(username)); } return resultIds; } }
Как только пользователь станет частью нашей системы, мы уведомим другие компоненты нашего приложения, опубликовав файл UserCreationEvent
.
Например, наше приложение выполняет две дополнительные операции всякий раз, когда мы запускаем такое UserCreationEvent
:
@Component public class ReportingListener { @EventListener(UserCreationEvent.class) public void reportUserCreation(UserCreationEvent event) { // e.g. increment a counter to report the total amount of new users System.out.println("Increment counter as new user was created: " + event); } @EventListener(UserCreationEvent.class) public void syncUserToExternalSystem(UserCreationEvent event) { // e.g. send a message to a messaging queue to inform other systems System.out.println("informing other systems about new user: " + event); } }
Запись и проверка событий приложения с помощью Spring Boot
Давайте напишем наш первый тест, который проверяет, UserService
генерирует событие всякий раз, когда мы создаем нового пользователя. Мы инструктируем Spring фиксировать наши события с помощью @RecordApplicationEvents
аннотации поверх нашего тестового класса:
@SpringBootTest @RecordApplicationEvents class UserServiceFullContextTest { @Autowired private ApplicationEvents applicationEvents; @Autowired private UserService userService; @Test void userCreationShouldPublishEvent() { this.userService.createUser("duke"); assertEquals(1, applicationEvents .stream(UserCreationEvent.class) .filter(event -> event.getUsername().equals("duke")) .count()); // There are multiple events recorded // PrepareInstanceEvent // BeforeTestMethodEvent // BeforeTestExecutionEvent // UserCreationEvent applicationEvents.stream().forEach(System.out::println); } }
После того, как мы выполняем публичный метод нашего класса испытываемый (createUser
из UserService
в этом примере), мы можем запросить все захваченные события из бинов ApplicationEvents
, которые мы внедряем в наш тест.
Открытый .stream()
метод класса ApplicationEvents
позволяет просмотреть все события, записанные для теста. Есть перегруженная версия .stream(),
в которой мы запрашиваем поток только определенных событий.
Несмотря на то, что мы генерируем только одно событие из нашего приложения, Spring захватывает четыре события для теста выше. Остальные три события относятся к Spring, как и PrepareInstanceEvent
в среде TestContext.
Поскольку мы используем JUnit Jupiter и SpringExtension
(зарегистрированный для нас при использовании @SpringBootTest
), мы также можем внедрить bean-компонент ApplicationEvents
в метод жизненного цикла JUnit или непосредственно в тест:
@Test void batchUserCreationShouldPublishEvents(@Autowired ApplicationEvents events) { List<Long> result = this.userService.createUser(List.of("duke", "mike", "alice")); assertEquals(3, result.size()); assertEquals(3, events.stream(UserCreationEvent.class).count()); }
Экземпляр ApplicationEvents
создается до и удаляется после каждого теста как часть текущего потока. Следовательно, вы даже можете использовать внедрение поля и @TestInstance(TestInstance.Lifecycle.PER_CLASS)
делить тестовый экземпляр между несколькими тестами ( PER_METHOD
по умолчанию).
Обратите внимание, что запуск всего контекста Spring @SpringBootTest
для такого теста может быть излишним. Мы также могли бы написать тест, который заполняет минимальный Spring TestContext
только нашим bean-компонентом UserService
, чтобы убедиться, что UserCreationEvent
опубликован:
@RecordApplicationEvents @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = UserService.class) class UserServicePerClassTest { @Autowired private ApplicationEvents applicationEvents; @Autowired private UserService userService; @Test void userCreationShouldPublishEvent() { this.userService.createUser("duke"); assertEquals(1, applicationEvents .stream(UserCreationEvent.class) .filter(event -> event.getUsername().equals("duke")) .count()); applicationEvents.stream().forEach(System.out::println); } }
… Или используйте альтернативный подход к тестированию.
Альтернативы тестированию весенних событий
В зависимости от того, чего вы хотите достичь с помощью теста, может быть достаточно проверить эту функциональность с помощью модульного теста:
@ExtendWith(MockitoExtension.class) class UserServiceUnitTest { @Mock private ApplicationEventPublisher applicationEventPublisher; @Captor private ArgumentCaptor<UserCreationEvent> eventArgumentCaptor; @InjectMocks private UserService userService; @Test void userCreationShouldPublishEvent() { Long result = this.userService.createUser("duke"); Mockito.verify(applicationEventPublisher).publishEvent(eventArgumentCaptor.capture()); assertEquals("duke", eventArgumentCaptor.getValue().getUsername()); } @Test void batchUserCreationShouldPublishEvents() { List<Long> result = this.userService.createUser(List.of("duke", "mike", "alice")); Mockito .verify(applicationEventPublisher, Mockito.times(3)) .publishEvent(any(UserCreationEvent.class)); } }
Обратите внимание, что здесь мы не используем никакой поддержки Spring Test и полагаемся исключительно на Mockito и JUnit Jupiter.
Другой подход заключается в том, чтобы не проверять события публикации явно, а проверять весь сценарий использования с помощью интеграционного теста:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ApplicationIT { @Autowired private TestRestTemplate testRestTemplate; @Test void shouldCreateUserAndPerformReporting() { ResponseEntity<Void> result = this.testRestTemplate .postForEntity("/api/users", "duke", Void.class); assertEquals(201, result.getStatusCodeValue()); assertTrue(result.getHeaders().containsKey("Location"), "Response doesn't contain Location header"); // additional assertion to verify the counter was incremented // additional assertion that a new message is part of the queue } }
В этом случае нам нужно будет проверить результат работы наших слушателей событий и, например, проверить, что мы помещаем сообщение в очередь или увеличиваем счетчик.
Резюме тестирования событий Spring с помощью Spring Boot
Все различные подходы сводятся к тестированию поведения и состояния. Благодаря новой функции @RecordApplicationEvents в
Spring Test у нас может возникнуть соблазн провести больше поведенческих тестов и проверить внутреннюю часть нашей реализации. В общем, мы должны сосредоточиться на тестировании состояния (также известном как результат), поскольку оно поддерживает беспроблемный рефакторинг.
Представьте себе следующее: мы используем, ApplicationEvent
чтобы разделять части нашего приложения и гарантировать, что это событие запускается во время теста. Через две недели мы решаем убрать / переработать эту развязку (по каким-то причинам). Наш вариант использования может по-прежнему работать, как ожидалось, но наш тест теперь не проходит, потому что мы делаем предположения о технической реализации, проверяя, сколько событий мы опубликовали.
Помните об этом и не перегружайте свои тесты деталями реализации (если вы хотите провести рефакторинг в будущем :). Тем не менее, есть определенные тестовые сценарии, когда функция @RecordApplicationEvents
очень помогает.
Исходный код со всеми альтернативными вариантами для тестирования Spring Event с помощью Spring Boot доступен на GitHub.
ссылка на оригинал статьи https://habr.com/ru/post/541028/
Добавить комментарий