Это уже третья статья, в которой описывается, как тестировать асинхронные операции с каркасом Byteman в приложении с использованием каркаса Spring. Статья посвящена тому, как выполнить такое тестирование с помощью библиотеки JUnit 5.
Предыдущая статья была посвящена тому, как такое тестирование можно выполнить с использованием библиотеки JUnit 4 ( первая статья ) и инфраструктуры Spock ( вторая статья ).
Как и во второй статье, нет необходимости читать предыдущие статьи, потому что все основные части будут повторяться. Тем не менее, я призываю вас прочитать предыдущую статью, касающуюся фреймворка Спока, потому что, на мой взгляд, это хорошая альтернатива для библиотеки JUnit 5.
Хотя весь демонстрационный проект имеет некоторые изменения по сравнению с тем, который использовался в предыдущих статьях (среди прочего, зависимость Spring Boot была обновлена с версии 1.5.3 до 2.2.4), исходный код, используемый для представления тестового примера, представляет собой одно и тоже. Поэтому, если вы уже прочитали предыдущую статью и все еще помните концепцию тестового примера, вы можете быстро перейти к разделу «Тестовый код».
Итак, начнем!
В этой статье мы обсудим, как тестировать операции в приложении, которое использует контекст Spring (с включенными асинхронными операциями). Нам не нужно менять производственный код, чтобы достичь этого.
Тесты будут выполняться с библиотекой JUnit 5. Если вы не знакомы с JUnit 5, я бы посоветовал вам прочитать руководство пользователя .
Вот некоторые другие полезные ссылки, которые могут дать вам краткое введение в JUnit 5:
Наша основная тестовая зависимость — библиотека BMUnit. Он является частью проекта Byteman и поддерживает библиотеку JUnit 5. Нам также нужно использовать модуль «utils» maven из проекта расширения BMUnit.
Byteman — это инструмент, который внедряет код Java в методы вашего приложения или в методы времени выполнения Java без необходимости перекомпиляции, перепаковки или даже повторного развертывания приложения.
Вам также может понравиться: Использование Byteman для выяснения причин изменения часового пояса на сервере приложений Java.
BMUnit — это пакет, который упрощает использование Byteman в качестве инструмента тестирования, интегрируя его в две самые популярные среды тестирования Java, JUnit и TestNG.
Расширение Bmunit — это небольшой проект на GitHub, содержащий правило JUnit 4, которое позволяет интегрировать его с платформой Byteman и тестами JUnit и Spock. Он также содержит несколько вспомогательных методов в методе «utils», который совместим с версией 4.0.10 проекта Byteman.
Как упоминалось ранее в статье, мы собираемся использовать код из демонстрационного приложения, которое является частью проекта «Bmunit-extension».
Исходный код можно найти на странице https://github.com/starnowski/bmunit-extension/tree/master/junit5-spring-demo .
Прецедент
Тестовый пример предполагает, что мы зарегистрировали нового пользователя в нашем приложении (все транзакции были совершены) и отправили ему электронное письмо. Письмо отправляется асинхронно. Теперь приложение содержит несколько тестов, которые показывают, как можно проверить этот случай. Не предполагается, что код, реализованный в демонстрационном приложении для Bmunit-расширения, является единственным или даже лучшим подходом.
Основная цель этого проекта — показать, как такой случай можно протестировать без изменения производственного кода при использовании Byteman.
В нашем примере нам нужно проверить процесс регистрации пользователя нового приложения. Предположим, что приложение позволяет регистрировать пользователя через REST API. Итак, клиент отправляет запрос с данными пользователя. Затем контроллер обрабатывает запрос.
Когда транзакция базы данных произошла, но до того, как ответ был установлен, контроллер вызывает Asynchronous Executor, чтобы отправить электронное письмо пользователю со ссылкой для регистрации (для подтверждения его адреса электронной почты).
Весь процесс представлен на диаграмме последовательности ниже.
Теперь я предполагаю, что это может быть не лучшим подходом для регистрации пользователей. Вероятно, было бы лучше использовать некоторый компонент планировщика, который проверяет, есть ли электронное письмо для отправки — не говоря уже о том, что для более крупных приложений отдельный микросервис был бы более подходящим.
Предположим, что это приемлемо для приложения, в котором нет проблем с доступными потоками.
Реализация для нашего REST Controller:
Джава
1
2
public class UserController {
3
5
private UserService service;
6
8
"/users") (
9
public UserDto post( UserDto dto)
10
{
11
return service.registerUser(dto);
12
}
13
}
Сервис, который обрабатывает объект User:
Джава
xxxxxxxxxx
1
2
public class UserService {
3
5
private PasswordEncoder passwordEncoder;
6
7
private RandomHashGenerator randomHashGenerator;
8
9
private MailService mailService;
10
11
private UserRepository repository;
12
14
public UserDto registerUser(UserDto dto)
15
{
16
User user = new User().setEmail(dto.getEmail()).setPassword(passwordEncoder.encode(dto.getPassword())).setEmailVerificationHash(randomHashGenerator.compute());
17
user = repository.save(user);
18
UserDto response = new UserDto().setId(user.getId()).setEmail(user.getEmail());
19
mailService.sendMessageToNewUser(response, user.getEmailVerificationHash());
20
return response;
21
}
22
}
Сервис, который обрабатывает почтовые сообщения:
Джава
xxxxxxxxxx
1
2
public class MailService {
3
5
private MailMessageRepository mailMessageRepository;
6
7
private JavaMailSender emailSender;
8
9
private ApplicationEventPublisher applicationEventPublisher;
10
12
public void sendMessageToNewUser(UserDto dto, String emailVerificationHash)
13
{
14
MailMessage mailMessage = new MailMessage();
15
mailMessage.setMailSubject("New user");
16
mailMessage.setMailTo(dto.getEmail());
17
mailMessage.setMailContent(emailVerificationHash);
18
mailMessageRepository.save(mailMessage);
19
applicationEventPublisher.publishEvent(new NewUserEvent(mailMessage));
20
}
21
23
24
public void handleNewUserEvent(NewUserEvent newUserEvent)
25
{
26
SimpleMailMessage message = new SimpleMailMessage();
27
message.setTo(newUserEvent.getMailMessage().getMailTo());
28
message.setSubject(newUserEvent.getMailMessage().getMailSubject());
29
message.setText(newUserEvent.getMailMessage().getMailContent());
30
emailSender.send(message);
31
}
32
}
Тестовый код
Чтобы увидеть, как присоединить все зависимости Byteman и Bmunit-extension, пожалуйста, проверьте раздел «Как прикрепить проект» . Пожалуйста, также проверьте Проект Дескриптор фил е для демонстрационного приложения , чтобы указать правильные зависимости от Байтмен проекта с 4 -й версии.
Давайте перейдем к тесту некоторого кода:
Джава
xxxxxxxxxx
1
2
webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT) (
3
value = CLEAR_DATABASE_SCRIPT_PATH, (
4
config = (transactionMode = ISOLATED),
5
executionPhase = BEFORE_TEST_METHOD)
6
value = CLEAR_DATABASE_SCRIPT_PATH, (
7
config = (transactionMode = ISOLATED),
8
executionPhase = AFTER_TEST_METHOD)
9
10
public class UserControllerTest {
11
13
static GreenMailExtension greenMail = new GreenMailExtension(ServerSetup.verbose(ServerSetup.ALL)).withConfiguration(new GreenMailConfiguration().withUser("no.such.mail@gmail.com", "no.such.password"));
14
15
UserRepository userRepository;
16
17
TestRestTemplate testRestTemplate;
18
19
private int port;
20
"Should send mail message after correct user sign-up and wait until the async operation which is e-mail sending is complete") (
22
name = "{index}. expected e-mail is {0}") (
23
strings = {"szymon.doe@nosuch.domain.com", "john.doe@gmail.com","marry.doe@hotmail.com", "jack.black@aws.eu" }) (
24
verbose = true, bmunitVerbose = true) (
25
rules = { (
26
name = "signal thread waiting for mutex \"UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation\"", (
27
targetClass = "com.github.starnowski.bmunit.extension.junit5.spring.demo.services.MailService",
28
targetMethod = "handleNewUserEvent(com.github.starnowski.bmunit.extension.junit5.spring.demo.util.NewUserEvent)",
29
targetLocation = "AT EXIT",
30
action = "joinEnlist(\"UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation\")")
31
})
32
public void testShouldCreateNewUserAndSendMailMessageInAsyncOperation(String expectedEmail) throws IOException, URISyntaxException, MessagingException {
33
// given
34
assertThat(userRepository.findByEmail(expectedEmail)).isNull();
35
UserDto dto = new UserDto().setEmail(expectedEmail).setPassword("XXX");
36
RestTemplate restTemplate = new RestTemplate();
37
createJoin("UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation", 1);
38
assertEquals(0, greenMail.getReceivedMessages().length);
39
// when
41
UserDto responseEntity = restTemplate.postForObject(new URI("http://localhost:" + port + "/users/"), (Object) dto, UserDto.class);
42
joinWait("UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation", 1, 15000);
43
// then
45
assertThat(userRepository.findByEmail(expectedEmail)).isNotNull();
46
assertThat(greenMail.getReceivedMessages().length).isEqualTo(1);
47
assertThat(greenMail.getReceivedMessages()[0].getSubject()).contains("New user");
48
assertThat(greenMail.getReceivedMessages()[0].getAllRecipients()[0].toString()).contains(expectedEmail);
49
}
50
}
Тестовый класс должен содержать аннотацию типа ( WithByteman
строка 1 ) для загрузки правил Byteman. Аннотация BMRule является частью проекта BMUnit. Все параметры ( строка 26 ) , ( name
targetClass
строка 27 ), targetMethod
( строка 28 ), targetLocation
( строка 29 ) и action
( строка 30 ) относятся к конкретному разделу в разделе языка правил байтманов. Опции targetClass
, targetMethod
и targetLocation
используются для указанной точки в коде Java, после чего правило должно быть выполнено.
action
Опция определяет , что должно быть сделано после достижения точки правила.
Если вы хотите узнать больше о языке правил Byteman, обратитесь к руководству программиста .
Цель этого метода тестирования - подтвердить, что новый пользователь приложения может быть зарегистрирован через контроллер оставшегося API, и приложение отправляет пользователю электронное письмо с подробностями регистрации. Последнее, что важно, тест подтверждает, что метод, который запускает Asynchronous Executor, который отправляет электронную почту, запускается.
Для этого нам нужно использовать механизм «Столяр». Из «Руководства разработчика» для Byteman мы можем выяснить, что объединители полезны в ситуациях, когда необходимо убедиться, что поток не продолжается до тех пор, пока не завершится один или несколько связанных потоков.
Как правило, когда мы создаем joiner, нам нужно указать идентификацию и номер потока, к которому нужно присоединиться. В разделе given
( строка 33 ) мы выполняем BMUnitUtils#createJoin(Object, int)
( строка 37 ) для создания соединения UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation
с одним ожидаемым числом потоков. Мы ожидаем, что нить, ответственная за отправку, присоединится.
Чтобы добиться этого, нам нужно через набор аннотаций BMRule, чтобы после выхода из метода ( targetLocation
опция со значением «AT EXIT») было выполнено определенное действие, которое выполняет метод Helper#joinEnlist(Object key)
, этот метод не приостанавливает текущий поток, в котором он был вызван.
В when
разделе ( строка 40 ), кроме выполнения метода testes, мы вызываем BMUnitUtils#joinWait(Object, int, long)
приостановку тестового потока, чтобы дождаться, пока число присоединяемых потоков для присоединяемого не UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation
достигнет ожидаемого значения. В случае, когда не будет ожидаемого числа присоединяемых потоков, выполнение достигнет тайм-аута, и определенные исключения будут выброшены.
В разделе then
( строка 44 ) мы проверяем, был ли пользователь создан, и отправлялось ли письмо с правильным содержанием.
Этот тест может быть выполнен без изменения исходного кода благодаря Byteman.
Это также может быть сделано с помощью базового Java-механизма, но это также потребует изменений в исходном коде.
Во-первых, мы должны создать компонент с CountDownLatch
.
Джава
xxxxxxxxxx
1
2
public class DummyApplicationCountDownLatch implements IApplicationCountDownLatch{
3
private CountDownLatch mailServiceCountDownLatch;
5
7
public void mailServiceExecuteCountDownInHandleNewUserEventMethod() {
8
if (mailServiceCountDownLatch != null) {
9
mailServiceCountDownLatch.countDown();
10
}
11
}
12
14
public void mailServiceWaitForCountDownLatchInHandleNewUserEventMethod(int milliseconds) throws InterruptedException {
15
if (mailServiceCountDownLatch != null) {
16
mailServiceCountDownLatch.await(milliseconds, TimeUnit.MILLISECONDS);
17
}
18
}
19
21
public void mailServiceResetCountDownLatchForHandleNewUserEventMethod() {
22
mailServiceCountDownLatch = new CountDownLatch(1);
23
}
24
26
public void mailServiceClearCountDownLatchForHandleNewUserEventMethod() {
27
mailServiceCountDownLatch = null;
28
}
29
}
Также необходимо внести изменения, MailService
чтобы конкретные методы для типа DummyApplicationCountDownLatch
были выполнены.
Джава
xxxxxxxxxx
1
private IApplicationCountDownLatch applicationCountDownLatch;
2
4
public void sendMessageToNewUser(UserDto dto, String emailVerificationHash)
5
{
6
MailMessage mailMessage = new MailMessage();
7
mailMessage.setMailSubject("New user");
8
mailMessage.setMailTo(dto.getEmail());
9
mailMessage.setMailContent(emailVerificationHash);
10
mailMessageRepository.save(mailMessage);
11
applicationEventPublisher.publishEvent(new NewUserEvent(mailMessage));
12
}
13
15
16
public void handleNewUserEvent(NewUserEvent newUserEvent)
17
{
18
SimpleMailMessage message = new SimpleMailMessage();
19
message.setTo(newUserEvent.getMailMessage().getMailTo());
20
message.setSubject(newUserEvent.getMailMessage().getMailSubject());
21
message.setText(newUserEvent.getMailMessage().getMailContent());
22
emailSender.send(message);
23
applicationCountDownLatch.mailServiceExecuteCountDownInHandleNewUserEventMethod();
24
}
После применения этих изменений мы можем реализовать следующий тестовый класс:
Джава
xxxxxxxxxx
1
webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT) (
2
value = CLEAR_DATABASE_SCRIPT_PATH, (
3
config = (transactionMode = ISOLATED),
4
executionPhase = BEFORE_TEST_METHOD)
5
value = CLEAR_DATABASE_SCRIPT_PATH, (
6
config = (transactionMode = ISOLATED),
7
executionPhase = AFTER_TEST_METHOD)
8
9
public class UserControllerTest {
10
12
static GreenMailExtension greenMail = new GreenMailExtension(new ServerSetup(3025, (String)null, "smtp").setVerbose(true)).withConfiguration(new GreenMailConfiguration().withUser("no.such.mail@gmail.com", "no.such.password"));
13
14
UserRepository userRepository;
15
16
TestRestTemplate testRestTemplate;
17
18
private int port;
19
20
private IApplicationCountDownLatch applicationCountDownLatch;
21
23
public void tearDown()
24
{
25
applicationCountDownLatch.mailServiceClearCountDownLatchForHandleNewUserEventMethod();
26
}
27
"Should send mail message after correct user sign-up and wait until the async operation which is e-mail sending is complete") (
29
name = "{index}. expected e-mail is {0}") (
30
strings = {"szymon.doe@nosuch.domain.com", "john.doe@gmail.com","marry.doe@hotmail.com", "jack.black@aws.eu" }) (
31
public void testShouldCreateNewUserAndSendMailMessageInAsyncOperation(String expectedEmail) throws IOException, URISyntaxException, MessagingException, InterruptedException {
32
// given
33
assertThat(userRepository.findByEmail(expectedEmail)).isNull();
34
UserDto dto = new UserDto().setEmail(expectedEmail).setPassword("XXX");
35
RestTemplate restTemplate = new RestTemplate();
36
applicationCountDownLatch.mailServiceResetCountDownLatchForHandleNewUserEventMethod();
37
assertEquals(0, greenMail.getReceivedMessages().length);
38
// when
40
UserDto responseEntity = restTemplate.postForObject(new URI("http://localhost:" + port + "/users/"), (Object) dto, UserDto.class);
41
applicationCountDownLatch.mailServiceWaitForCountDownLatchInHandleNewUserEventMethod(15000);
42
// then
44
assertThat(userRepository.findByEmail(expectedEmail)).isNotNull();
45
assertThat(greenMail.getReceivedMessages().length).isEqualTo(1);
46
assertThat(greenMail.getReceivedMessages()[0].getSubject()).contains("New user");
47
assertThat(greenMail.getReceivedMessages()[0].getAllRecipients()[0].toString()).contains(expectedEmail);
48
}
49
}
Полный код для демонстрационного приложения можно найти на Github .
Резюме
Byteman позволяет тестировать асинхронные операции в приложении без изменения его исходного кода. Те же тесты могут быть протестированы без Byteman, но это потребует изменений в исходном коде.