{{announcement.body}}
{{announcement.title}}

Testing Asynchronous Operations in Spring With JUnit and Byteman

DZone 's Guide to

Testing Asynchronous Operations in Spring With JUnit and Byteman

Learn how to test such operations in an application that uses spring context (with asynchronous operations enabled).

· Performance Zone ·
Free Resource

person eating a donut

Learn more about JUnit and Byteman!

Testing asynchronous operations might cause some troubles and usually requires few challenges and also code changes (even in production code).

In this article, we can find how to test such operations in an application that uses spring context (with asynchronous operations enabled). We don’t have to change the production code to achieve this.

Tests are going to be run in JUnit 4. For tests, we are going to use features from the Byteman library.  We also gone have to attach the “Bmunit-extension” library, which gives contains JUnit rule and some helper methods used during our tests.

Byteman is a tool that injects Java code into your application methods or into Java runtime methods without the need for you to recompile, repackage, or even redeploy your application.

You may also like: Using Byteman to Find Out Why the TimeZone Changed on a Java App Server

BMUnit is a package that makes it simple to use Byteman as a testing tool by integrating it into the two most popular Java test frameworks, JUnit and TestNG.

The Bmunit-extension is a small project on GitHub, which contains junit4 rule, which allows integration with the Byteman framework and uses it in JUnit and Spock tests. And It contains a few helper methods.

In this article, we are going to use code from the demo application, which is part of the “Bmunit-extension” project. Source code can be found on https://github.com/starnowski/bmunit-extension/tree/feature/article_examples


Test case

Tests case assumes that we register a new application user (all transactions were committed) and sends an email message to him. The email message sending operation is asynchronous.

Now the application contains few tests which show how this case can be tested.

There is no suggestion that the code implemented in the demo application for the Bmunit-extension is the only approach and even the best one. The primary purpose of this project is to show how such a case could be tested without any production code change with usage of the Byteman library. 

In our example test, we would like to check the process of a new application user registration process. Let’s assume that the application allows user registration via Rest API. So Rest API client sends a request with user data. The Rest API controller is proceeding the request. After when the database, transaction is being committed, but before returning Rest API response, the controller invokes an Asynchronous Executor to send an email to a user with a registration link (to confirm email address).

The whole process is presented in the sequence diagram below. 




 

sequence diagram



Now I am guessing that this might not be the best approach to register users. Probably better would be to use some kind of scheduler component, which checks if there is an email to send. Not to mention that for larger applications, the separate microservice would be more suitable. Let’s assume that for an application that does not have a problem with available threads is okay.

Implementation contains Rest Controller:

Java




x
13


 
1
@RestController
2
public class UserController {
3
 
          
4
   @Autowired
5
   private UserService service;
6
 
          
7
   @ResponseBody
8
   @PostMapping("/users")
9
   public UserDto post(@RequestBody UserDto dto)
10
   {
11
       return service.registerUser(dto);
12
   }
13
}



Service which handles “User” object:

Java




xxxxxxxxxx
1
22


 
1
@Service
2
public class UserService {
3
 
          
4
   @Autowired
5
   private PasswordEncoder passwordEncoder;
6
   @Autowired
7
   private RandomHashGenerator randomHashGenerator;
8
   @Autowired
9
   private MailService mailService;
10
   @Autowired
11
   private UserRepository repository;
12
 
          
13
   @Transactional
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
}



A service which handles mail messages:

Java




xxxxxxxxxx
1
33


 
1
@Service
2
public class MailService {
3
 
          
4
   @Autowired
5
   private MailMessageRepository mailMessageRepository;
6
   @Autowired
7
   private JavaMailSender emailSender;
8
   @Autowired
9
   private ApplicationEventPublisher applicationEventPublisher;
10
 
          
11
   @Transactional
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
   @Async
23
   @TransactionalEventListener
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
}



Test code

To see how to attach all Byteman and Bmunit-extension dependencies, please check the section "How to attach project".

Let’s go to test code:

Java




xxxxxxxxxx
1
53


 
1
@RunWith(SpringRunner.class)
2
@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT)
3
@Sql(value = CLEAR_DATABASE_SCRIPT_PATH,
4
       config = @SqlConfig(transactionMode = ISOLATED),
5
       executionPhase = BEFORE_TEST_METHOD)
6
@Sql(value = CLEAR_DATABASE_SCRIPT_PATH,
7
       config = @SqlConfig(transactionMode = ISOLATED),
8
       executionPhase = AFTER_TEST_METHOD)
9
@EnableAsync
10
public class UserControllerTest {
11
 
          
12
   @Rule
13
   public BMUnitMethodRule bmUnitMethodRule = new BMUnitMethodRule();
14
   @Rule
15
   public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP_IMAP);
16
 
          
17
   @Autowired
18
   UserRepository userRepository;
19
   @Autowired
20
   TestRestTemplate restTemplate;
21
   @LocalServerPort
22
   private int port;
23
 
          
24
   @Test
25
   @BMUnitConfig(verbose = true, bmunitVerbose = true)
26
   @BMRules(rules = {
27
           @BMRule(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
 
          
41
       // when
42
       UserDto responseEntity = restTemplate.postForObject(new URI("http://localhost:" + port + "/users"), (Object) dto, UserDto.class);
43
       joinWait("UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation", 1, 15000);
44
 
          
45
       // 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
 
          
52
}



Test class needs to contain an object of type “BMUnitMethodRule” (line 13) to load Byteman rules. 

The BMRule annotation is part of the BMUnit project. All options “name”, “targetClass“ (line 28), “targetMethod“ (line 29), “targetLocation“ (line 30) and “action“ (line 31) refers to the specific section in Byteman rule language section. Options “targetClass“, “targetMethod“ and “targetLocation“ are used to a specified point in java code, after which the rule should be executed.

The “action” option defines what should be done after reaching the rule point.

If you would like to know more about the Byteman rule language, then please check Programer’s guide.

The purpose of this test method is to confirm that the new application user can be registered via the rest API controller, and the application sends an email to the user with registration details. The last important thing, the test confirms that the method which triggers an Asynchronous Executor which sends an email is being triggered.

To do that, we need to use a “Joiner” mechanism. From “Developer Guide” for Byteman, we can find out that the joiners are useful in situations where it is necessary to ensure that a thread does not proceed until one or more related threads have exited.

Generally, when we create joiner, we need to specify the identification and number of thread which needs to join. In the “given” (line 34) section we executes “BMUnitUtils#createJoin(Object, int)” to create “UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation” joiner with one as expected number of threads. We expect that the thread responsible for sending is going to join.

To achieve this, we need to via BMRule annotation set that after method exit (“targetLocation” option with value “AT EXIT”) the specific action need be done which executes method “Helper#joinEnlist(Object key)”, this method does not suspend current thread in which it was called.

In the “when” section (line 41), besides executing testes method, we invoke “BMUnitUtils#joinWait(Object, int, long)” to suspend test thread to wait until the number of joined threads for joiner “UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation” reach expected value. In case when there won’t be expected number of joined threads, then execution is going to reach a timeout, and certain exceptions is going to be thrown.

In the “then” (line 45) section, we check if the user was created, and email with correct content was sent. 

This test could be done without changing source code thanks to Byteman.

It also could be done with basic java mechanism, but it would also require changes in source code.

First, we have to create a component with “CountDownLatch”.

Java




xxxxxxxxxx
1
30


 
1
@Component
2
public class DummyApplicationCountDownLatch implements IApplicationCountDownLatch{
3
 
          
4
   private CountDownLatch mailServiceCountDownLatch;
5
 
          
6
   @Override
7
   public void mailServiceExecuteCountDownInHandleNewUserEventMethod() {
8
       if (mailServiceCountDownLatch != null) {
9
           mailServiceCountDownLatch.countDown();
10
       }
11
   }
12
 
          
13
   @Override
14
   public void mailServiceWaitForCountDownLatchInHandleNewUserEventMethod(int milliseconds) throws InterruptedException {
15
       if (mailServiceCountDownLatch != null) {
16
           mailServiceCountDownLatch.await(milliseconds, TimeUnit.MILLISECONDS);
17
       }
18
   }
19
 
          
20
   @Override
21
   public void mailServiceResetCountDownLatchForHandleNewUserEventMethod() {
22
       mailServiceCountDownLatch = new CountDownLatch(1);
23
   }
24
 
          
25
   @Override
26
   public void mailServiceClearCountDownLatchForHandleNewUserEventMethod() {
27
       mailServiceCountDownLatch = null;
28
   }
29
}



There are also changes required in “MailService” so that the specific methods for type DummyApplicationCountDownLatch would be executed. 

Java




xxxxxxxxxx
1
25


 
1
@Autowired
2
private IApplicationCountDownLatch applicationCountDownLatch;
3
 
          
4
@Transactional
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
@Async
16
@TransactionalEventListener
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
}



After applying those changes we can implement below test class:

Java




xxxxxxxxxx
1
51


 
1
@RunWith(SpringRunner.class)
2
@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT)
3
@Sql(value = CLEAR_DATABASE_SCRIPT_PATH,
4
       config = @SqlConfig(transactionMode = ISOLATED),
5
       executionPhase = BEFORE_TEST_METHOD)
6
@Sql(value = CLEAR_DATABASE_SCRIPT_PATH,
7
       config = @SqlConfig(transactionMode = ISOLATED),
8
       executionPhase = AFTER_TEST_METHOD)
9
@EnableAsync
10
public class UserControllerTest {
11
 
          
12
   @Rule
13
   public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP_IMAP);
14
 
          
15
   @Autowired
16
   UserRepository userRepository;
17
   @Autowired
18
   TestRestTemplate restTemplate;
19
   @LocalServerPort
20
   private int port;
21
   @Autowired
22
   private IApplicationCountDownLatch applicationCountDownLatch;
23
 
          
24
   @After
25
   public void tearDown()
26
   {
27
       applicationCountDownLatch.mailServiceClearCountDownLatchForHandleNewUserEventMethod();
28
   }
29
 
          
30
   @Test
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
 
          
39
       // when
40
       UserDto responseEntity = restTemplate.postForObject(new URI("http://localhost:" + port + "/users"), (Object) dto, UserDto.class);
41
       applicationCountDownLatch.mailServiceWaitForCountDownLatchInHandleNewUserEventMethod(15000);
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
 
          
50
}



Summary

The Byteman allows for testing asynchronous operations in an application without changing its source code. The same test cases can be tested without the Byteman, but it would require changes in source code.


Further Reading

Enabling Bytemn Script With Red Hat JBoss Fuse and AMQ

Testing Made Easy With These Top 7 Testing Tools

Unit Testing With Mockito

Topics:
java (programming lang... ,byteman script ,junit 4 ,spring ,asynchronous testing ,performance

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}