Тестирование асинхронных операций может вызвать некоторые проблемы и обычно требует небольшого количества проблем, а также изменений кода (даже в рабочем коде).
В этой статье мы узнаем, как тестировать такие операции в приложении, которое использует контекст Spring (с включенными асинхронными операциями). Нам не нужно менять производственный код, чтобы достичь этого.
Тесты будут запускаться в JUnit 4. Для тестов мы будем использовать функции из библиотеки Byteman. Нам также пришлось присоединить библиотеку «Bmunit-extension», которая содержит правило JUnit и некоторые вспомогательные методы, используемые во время наших тестов.
Byteman — это инструмент, который внедряет код Java в методы вашего приложения или в методы времени выполнения Java без необходимости перекомпиляции, перепаковки или даже повторного развертывания приложения.
Вам также может понравиться: Использование Byteman для выяснения причин изменения часового пояса на сервере приложений Java.
BMUnit — это пакет, который упрощает использование Byteman в качестве инструмента тестирования, интегрируя его в две самые популярные среды тестирования Java, JUnit и TestNG .
Расширение Bmunit — это небольшой проект на GitHub, который содержит правило junit4, которое позволяет интегрировать его с платформой Byteman и использовать его в тестах JUnit и Spock. И содержит несколько вспомогательных методов.
В этой статье мы собираемся использовать код из демонстрационного приложения, которое является частью проекта «Bmunit-extension». Исходный код можно найти по адресу https://github.com/starnowski/bmunit-extension/tree/feature/article_examples .
Прецедент
Тестовый пример предполагает, что мы регистрируем нового пользователя приложения (все транзакции были совершены) и отправляет ему сообщение электронной почты. Операция отправки сообщения электронной почты является асинхронной.
Теперь приложение содержит несколько тестов, которые показывают, как этот случай может быть проверен.
Не предполагается, что код, реализованный в демонстрационном приложении для расширения Bmunit, является единственным и даже лучшим. Основная цель этого проекта — показать, как можно проверить такой случай без каких-либо изменений производственного кода с использованием библиотеки Byteman.
В нашем тестовом примере мы хотели бы проверить процесс регистрации пользователя нового приложения. Предположим, что приложение позволяет регистрировать пользователя через Rest API . Поэтому клиент Rest API отправляет запрос с данными пользователя. Контроллер Rest API обрабатывает запрос. После того, как база данных, транзакция фиксируется, но перед возвратом ответа 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
}
Сервис, который обрабатывает объект «Пользователь»:
Джава
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, пожалуйста, проверьте раздел «Как прикрепить проект» .
Пойдем тестировать код:
Джава
xxxxxxxxxx
1
SpringRunner.class) (
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
public BMUnitMethodRule bmUnitMethodRule = new BMUnitMethodRule();
14
15
public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP_IMAP);
16
18
UserRepository userRepository;
19
20
TestRestTemplate restTemplate;
21
22
private int port;
23
25
verbose = true, bmunitVerbose = true) (
26
rules = { (
27
name = "signal thread waiting for mutex \"UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation\"", (
28
targetClass = "com.github.starnowski.bmunit.extension.junit4.spock.spring.demo.services.MailService",
29
targetMethod = "handleNewUserEvent(com.github.starnowski.bmunit.extension.junit4.spock.spring.demo.util.NewUserEvent)",
30
targetLocation = "AT EXIT",
31
action = "joinEnlist(\"UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation\")")
32
})
33
public void shouldCreateNewUserAndSendMailMessageInAsyncOperation() throws IOException, URISyntaxException, MessagingException {
34
// given
35
String expectedEmail = "szymon.doe@nosuch.domain.com";
36
assertThat(userRepository.findByEmail(expectedEmail)).isNull();
37
UserDto dto = new UserDto().setEmail(expectedEmail).setPassword("XXX");
38
createJoin("UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation", 1);
39
assertEquals(0, greenMail.getReceivedMessages().length);
40
// when
42
UserDto responseEntity = restTemplate.postForObject(new URI("http://localhost:" + port + "/users"), (Object) dto, UserDto.class);
43
joinWait("UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation", 1, 15000);
44
// then
46
assertThat(userRepository.findByEmail(expectedEmail)).isNotNull();
47
assertThat(greenMail.getReceivedMessages().length).isEqualTo(1);
48
assertThat(greenMail.getReceivedMessages()[0].getSubject()).contains("New user");
49
assertThat(greenMail.getReceivedMessages()[0].getAllRecipients()[0].toString()).contains(expectedEmail);
50
}
51
}
Класс испытаний должен содержать объект типа «BMUnitMethodRule» ( л ине 13 ) , чтобы загрузить Байтмен правила.
Аннотация BMRule является частью проекта BMUnit. Все опции «название», «targetClass» ( л ине 28 ), «targetMethod» ( л ине 29 ), «targetLocation» ( л ине 30 ) и «действие» ( л ине 31 ) относится к разделу определенного в Байтмен языке правил раздел. Параметры «targetClass», «targetMethod« и «targetLocation» используются для указанной точки в коде Java, после чего правило должно выполняться.
Опция «action» определяет, что должно быть сделано после достижения правила.
Если вы хотите узнать больше о языке правил Byteman, обратитесь к руководству программиста .
Цель этого метода тестирования - подтвердить, что новый пользователь приложения может быть зарегистрирован через контроллер оставшегося API, и приложение отправляет пользователю электронное письмо с подробностями регистрации. Последнее, что важно, тест подтверждает, что метод, который запускает Asynchronous Executor, который отправляет электронную почту, запускается.
Для этого нам нужно использовать механизм «Столяр». Из «Руководства разработчика» для Byteman мы можем выяснить, что объединители полезны в ситуациях, когда необходимо убедиться, что поток не продолжается до тех пор, пока не завершится один или несколько связанных потоков.
Как правило, когда мы создаем joiner, нам нужно указать идентификацию и номер потока, к которому нужно присоединиться. В «данной» ( л ине 34 ) разделе выполняет «BMUnitUtils # createJoin (объект, целое)» , чтобы создать «UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation» столяра с одним как ожидаемое число нитей. Мы ожидаем, что нить, ответственная за отправку, присоединится.
Чтобы добиться этого, нам нужно с помощью аннотации BMRule установить, что после выхода из метода (опция «targetLocation» со значением «AT EXIT») необходимо выполнить определенное действие, которое выполняет метод «Helper # joinEnlist (Object key)», этот метод не выполняет приостановить текущий поток, в котором он был вызван.
В «когда» секции ( л иня 41 ), помимо выполнения методы семенников, мы вызываем «BMUnitUtils # joinWait (Object, INT, длинный)» приостановить тестовую нить не ждать , пока количество соединенных нитей для столяра «UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation» достичь ожидаемого значения. В случае, когда не будет ожидаемого числа присоединяемых потоков, выполнение достигнет тайм-аута, и будут выданы определенные исключения.
В «тогда» ( л ине 45 раздела), мы проверяем , если пользователь был создан, и электронная почта с правильным содержанием было отправлено.
Этот тест может быть выполнен без изменения исходного кода благодаря 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
2
private IApplicationCountDownLatch applicationCountDownLatch;
3
5
public void sendMessageToNewUser(UserDto dto, String emailVerificationHash)
6
{
7
MailMessage mailMessage = new MailMessage();
8
mailMessage.setMailSubject("New user");
9
mailMessage.setMailTo(dto.getEmail());
10
mailMessage.setMailContent(emailVerificationHash);
11
mailMessageRepository.save(mailMessage);
12
applicationEventPublisher.publishEvent(new NewUserEvent(mailMessage));
13
}
14
16
17
public void handleNewUserEvent(NewUserEvent newUserEvent)
18
{
19
SimpleMailMessage message = new SimpleMailMessage();
20
message.setTo(newUserEvent.getMailMessage().getMailTo());
21
message.setSubject(newUserEvent.getMailMessage().getMailSubject());
22
message.setText(newUserEvent.getMailMessage().getMailContent());
23
emailSender.send(message);
24
applicationCountDownLatch.mailServiceExecuteCountDownInHandleNewUserEventMethod();
25
}
После применения этих изменений мы можем реализовать следующий тестовый класс:
Джава
xxxxxxxxxx
1
SpringRunner.class) (
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
public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP_IMAP);
14
16
UserRepository userRepository;
17
18
TestRestTemplate restTemplate;
19
20
private int port;
21
22
private IApplicationCountDownLatch applicationCountDownLatch;
23
25
public void tearDown()
26
{
27
applicationCountDownLatch.mailServiceClearCountDownLatchForHandleNewUserEventMethod();
28
}
29
31
public void shouldCreateNewUserAndSendMailMessageInAsyncOperation() throws IOException, URISyntaxException, MessagingException, InterruptedException {
32
// given
33
String expectedEmail = "szymon.doe@nosuch.domain.com";
34
assertThat(userRepository.findByEmail(expectedEmail)).isNull();
35
UserDto dto = new UserDto().setEmail(expectedEmail).setPassword("XXX");
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
}
Резюме
Byteman позволяет тестировать асинхронные операции в приложении без изменения его исходного кода. Те же тесты могут быть протестированы без Byteman, но это потребует изменений в исходном коде.
Дальнейшее чтение
Включение сценария Bytemn с помощью Red Hat JBoss Fuse и AMQ
Тестирование стало проще с помощью этих 7 лучших инструментов тестирования