Одна из основных функций 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
создает и хранит наших новых
пользователей.Мы можем создать как одного пользователя, так и
группу пользователей:
@Servicepublic 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
:
@Componentpublic 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@RecordApplicationEventsclass 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 или непосредственно в тест:
@Testvoid 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
для такого тестаможет быть
излишним.Мы также могли бы написать тест, который заполняет
минимальный SpringTestContext
только
нашим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.