Одна из основных функций 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);
}
}
… Или используйте альтернативный подход к тестированию.
Альтернативы тестированию событий Spring
В зависимости от того, чего вы хотите достичь с помощью теста, может быть достаточно проверить эту функциональность с помощью модульного теста:
@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.