Это вторая статья, в которой описывается, как тестировать асинхронные операции с платформой Byteman в приложении с использованием среды Spring. В статье обсуждается, как реализовать инфраструктуру Spock, а в первой статье «Тестирование асинхронных операций весной с помощью JUnit и Byteman» основное внимание уделяется реализации тестов с помощью JUnit4.
Нет необходимости читать предыдущую статью, потому что все основы будут повторяться. Тем не менее, я рекомендую вам прочитать эту статью, если вы заинтересованы в тестировании асинхронных операций с JUnit.
Если вы уже прочитали предыдущую статью и все еще помните концепцию тестового примера, вы можете быстро перейти к разделу «Тестирование кода».
Итак, начнем!
В этой статье мы обсудим, как тестировать операции в приложении, которое использует контекст Spring (с включенными асинхронными операциями). Нам не нужно менять производственный код, чтобы достичь этого.
Испытания будут проводиться в Споке. Если вы не знакомы со Споком, я бы посоветовал вам прочитать его документацию .
Вот несколько других полезных ссылок, которые могут дать вам краткое представление о Споке:
Для тестов мы будем использовать функции из библиотеки Byteman.
Нам также нужно присоединить библиотеку «Bmunit-extension», которая содержит правила JUnit и некоторые вспомогательные методы, используемые во время наших тестов. Хотя BMUnitMethodRule
правило было реализовано для JUnit4, его можно использовать и в Spock.
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. Итак, клиент отправляет запрос с данными пользователя. Затем контроллер обрабатывает запрос.
Когда транзакция базы данных произошла, но до того, как ответ был установлен, контроллер вызывает Asynchronous Executor, чтобы отправить электронное письмо пользователю со ссылкой для регистрации (для подтверждения его адреса электронной почты).
Весь процесс представлен на диаграмме последовательности ниже.
Теперь я предполагаю, что это может быть не лучшим подходом для регистрации пользователей. Вероятно, было бы лучше использовать некоторый компонент планировщика, который проверяет, есть ли электронное письмо для отправки — не говоря уже о том, что для более крупных приложений отдельный микросервис был бы более подходящим.
Давайте предположим, что для приложения, которое не имеет проблемы с доступными потоками.
Реализация для нашего REST Controller:
Джава
xxxxxxxxxx
1
2
public class UserController {
3
4
5
private UserService service;
6
7
8
"/users") (
9
public UserDto post( UserDto dto)
10
{
11
return service.registerUser(dto);
12
}
13
}
Сервис, который обрабатывает User
объект:
Джава
xxxxxxxxxx
1
2
public class UserService {
3
4
5
private PasswordEncoder passwordEncoder;
6
7
private RandomHashGenerator randomHashGenerator;
8
9
private MailService mailService;
10
11
private UserRepository repository;
12
13
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
4
5
private MailMessageRepository mailMessageRepository;
6
7
private JavaMailSender emailSender;
8
9
private ApplicationEventPublisher applicationEventPublisher;
10
11
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
22
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, пожалуйста, проверьте раздел «Как прикрепить проект» .
Давайте проверим некоторый код:
Groovy
xxxxxxxxxx
1
value = CLEAR_DATABASE_SCRIPT_PATH, ([ (
2
config = (transactionMode = ISOLATED),
3
executionPhase = BEFORE_TEST_METHOD),
4
value = CLEAR_DATABASE_SCRIPT_PATH, (
5
config = (transactionMode = ISOLATED),
6
executionPhase = AFTER_TEST_METHOD)])
7
8
webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT) (
9
class UserControllerSpockItTest extends Specification {
10
11
12
public BMUnitMethodRule bmUnitMethodRule = new BMUnitMethodRule()
13
14
public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP_IMAP)
15
16
17
UserRepository userRepository
18
19
TestRestTemplate restTemplate
20
21
private int port
22
23
24
verbose = true, bmunitVerbose = true) (
25
rules = [ (
26
name = "signal thread waiting for mutex \"UserControllerSpockItTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation\"", (
27
targetClass = "com.github.starnowski.bmunit.extension.junit4.spock.spring.demo.services.MailService",
28
targetMethod = "handleNewUserEvent(com.github.starnowski.bmunit.extension.junit4.spock.spring.demo.util.NewUserEvent)",
29
targetLocation = "AT EXIT",
30
action = "joinEnlist(\"UserControllerSpockItTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation\")")]
31
)
32
def "should send mail message and wait until async operation is completed"() throws IOException, MessagingException {
33
given:
34
assertThat(userRepository.findByEmail(expectedEmail)).isNull();
35
UserDto dto = new UserDto().setEmail(expectedEmail).setPassword("XXX");
36
createJoin("UserControllerSpockItTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation", 1);
37
assertEquals(0, greenMail.getReceivedMessages().length);
38
39
when:
40
UserDto responseEntity = restTemplate.postForObject(new URI("http://localhost:" + port + "/users"), (Object) dto, UserDto.class);
41
joinWait("UserControllerSpockItTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation", 1, GROOVY_TEST_ASYNC_OPERATION_TIMEOUT);
42
43
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
where:
50
expectedEmail << ["szymon.doe@nosuch.domain.com", "john.doe@gmail.com","marry.doe@hotmail.com", "jack.black@aws.eu"]
51
}
52
}
Тестовый класс должен содержать объект типа BMUnitMethodRule
( строка 12 ) для загрузки правил Byteman.
Аннотация BMRule является частью проекта BMUnit. Все варианты, в том числе name
, targetClass
( линия 27 ), targetMethod
( линия 28 ), targetLocation
( строка 29 ) и action
( линия 30 ) относится к разделу определенного в разделе Байтмен языка правила. Параметры, targetClass
, targetMethod
и targetLocation
используются в заданной точке в Java коде, после чего правило должно выполняться.
action
Опция определяет , что должно быть сделано после достижения точки правила. Если вы хотите узнать больше о языке правил Byteman, обратитесь к руководству для разработчиков .
Цель этого метода тестирования — подтвердить, что новый пользователь приложения может быть зарегистрирован через контроллер REST API и что приложение отправляет пользователю электронное письмо с подробностями регистрации. Последнее, что тест подтверждает, — метод запускает асинхронного исполнителя, который отправляет электронное письмо.
Для этого нам нужно использовать механизм «Joiner». Из Руководства разработчика по Byteman мы можем выяснить, что Joiners полезны в ситуациях, когда необходимо убедиться, что поток не продолжается до тех пор, пока один или несколько связанных потоков вышел.
Обычно, когда мы создаем Joiner, нам нужно указать идентификацию и номер нити, к которой нужно присоединиться. В разделе given
( строка 33 ) мы выполняем команду BMUnitUtils#createJoin(Object, int)
create: UserControllerSpockItTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation
joiner с единицей в качестве ожидаемого количества потоков. Мы ожидаем, что нить, ответственная за отправку, присоединится.
Чтобы достичь этого, нам нужно (через аннотацию BMRule) установить его после выхода из метода ( targetLocation
опция со значением «AT EXIT»). Затем необходимо выполнить определенное действие, которое выполняется Helper#joinEnlist(Object key)
. Этот метод не приостанавливает текущий поток, в котором он был вызван.
В when
разделе ( строка 39 ), помимо выполнения метода testes, мы вызываем BMUnitUtils#joinWait(Object, int, long)
приостановку тестового потока, чтобы дождаться, пока число присоединившихся потоков для Joiner UserControllerSpockItTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation
достигнет ожидаемого значения. В случае, когда не будет ожидаемого числа присоединяемых потоков, выполнение достигнет тайм-аута, и будут выданы определенные исключения.
В разделе then
( строка 43 ) мы проверяем, был ли пользователь создан, и отправлялось ли письмо с правильным содержанием.
Этот тест может быть выполнен без изменения исходного кода благодаря Byteman. Это также может быть сделано с помощью базового Java-механизма, но это также потребует изменений в исходном коде.
Во-первых, мы должны создать компонент с CountDownLatch
.
Джава
xxxxxxxxxx
1
2
public class DummyApplicationCountDownLatch implements IApplicationCountDownLatch{
3
4
private CountDownLatch mailServiceCountDownLatch;
5
6
7
public void mailServiceExecuteCountDownInHandleNewUserEventMethod() {
8
if (mailServiceCountDownLatch != null) {
9
mailServiceCountDownLatch.countDown();
10
}
11
}
12
13
14
public void mailServiceWaitForCountDownLatchInHandleNewUserEventMethod(int milliseconds) throws InterruptedException {
15
if (mailServiceCountDownLatch != null) {
16
mailServiceCountDownLatch.await(milliseconds, TimeUnit.MILLISECONDS);
17
}
18
}
19
20
21
public void mailServiceResetCountDownLatchForHandleNewUserEventMethod() {
22
mailServiceCountDownLatch = new CountDownLatch(1);
23
}
24
25
26
public void mailServiceClearCountDownLatchForHandleNewUserEventMethod() {
27
mailServiceCountDownLatch = null;
28
}
29
}
Существуют также изменения, необходимые для того, MailService
чтобы были выполнены конкретные методы для типа DummyApplicationCountDownLatch
.
Джава
xxxxxxxxxx
1
2
private IApplicationCountDownLatch applicationCountDownLatch;
3
4
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
15
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
}
После применения этих изменений мы можем реализовать следующий тестовый класс:
Groovy
xxxxxxxxxx
1
value = CLEAR_DATABASE_SCRIPT_PATH, ([ (
2
config = (transactionMode = ISOLATED),
3
executionPhase = BEFORE_TEST_METHOD),
4
value = CLEAR_DATABASE_SCRIPT_PATH, (
5
config = (transactionMode = ISOLATED),
6
executionPhase = AFTER_TEST_METHOD)])
7
8
webEnvironment= RANDOM_PORT) (
9
class UserControllerSpockItTest extends Specification {
10
11
12
public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP_IMAP)
13
14
15
UserRepository userRepository
16
17
TestRestTemplate restTemplate
18
19
private int port
20
21
private IApplicationCountDownLatch applicationCountDownLatch
22
23
def cleanup() {
24
applicationCountDownLatch.mailServiceClearCountDownLatchForHandleNewUserEventMethod()
25
}
26
27
28
def "should send mail message and wait until async operation is completed, the test e-mail is #expectedEmail"() throws IOException, MessagingException {
29
given:
30
assertThat(userRepository.findByEmail(expectedEmail)).isNull()
31
UserDto dto = new UserDto().setEmail(expectedEmail).setPassword("XXX")
32
applicationCountDownLatch.mailServiceResetCountDownLatchForHandleNewUserEventMethod()
33
assertEquals(0, greenMail.getReceivedMessages().length)
34
35
when:
36
restTemplate.postForObject(new URI("http://localhost:" + port + "/users"), (Object) dto, UserDto.class)
37
applicationCountDownLatch.mailServiceWaitForCountDownLatchInHandleNewUserEventMethod(15000)
38
39
then:
40
assertThat(userRepository.findByEmail(expectedEmail)).isNotNull()
41
assertThat(greenMail.getReceivedMessages().length).isEqualTo(1)
42
assertThat(greenMail.getReceivedMessages()[0].getSubject()).contains("New user")
43
assertThat(greenMail.getReceivedMessages()[0].getAllRecipients()[0].toString()).contains(expectedEmail)
44
45
where:
46
expectedEmail << ["szymon.doe@nosuch.domain.com", "john.doe@gmail.com","marry.doe@hotmail.com", "jack.black@aws.eu"]
47
}
48
}
Резюме
Byteman позволяет тестировать асинхронные операции в приложении без изменения его исходного кода. Те же тесты могут быть протестированы без Byteman, но это потребует изменений в исходном коде.
Дальнейшее чтение
Включение сценария Bytemn с помощью Red Hat JBoss Fuse и AMQ
Тестирование стало проще с помощью этих 7 лучших инструментов тестирования