DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Testing Asynchronous Operations in Spring With JUnit 5 and Byteman
  • Testing Asynchronous Operations in Spring With Spock and Byteman
  • Mastering Unit Testing and Test-Driven Development in Java
  • Comprehensive Guide to Unit Testing Spring AOP Aspects

Trending

  • Why Database Migrations Take Months and How to Speed Them Up
  • Beyond Simple Responses: Building Truly Conversational LLM Chatbots
  • Build a Simple REST API Using Python Flask and SQLite (With Tests)
  • MySQL to PostgreSQL Database Migration: A Practical Case Study
  1. DZone
  2. Coding
  3. Java
  4. Testing Asynchronous Operations in Spring With JUnit and Byteman

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).

By 
Szymon Tarnowski user avatar
Szymon Tarnowski
DZone Core CORE ·
Updated Jan. 21, 20 · Tutorial
Likes (11)
Comment
Save
Tweet
Share
34.6K Views

Join the DZone community and get the full member experience.

Join For Free

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.

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

Unit Testing With Mockito

unit test JUnit application Java (programming language) Spring Framework

Opinions expressed by DZone contributors are their own.

Related

  • Testing Asynchronous Operations in Spring With JUnit 5 and Byteman
  • Testing Asynchronous Operations in Spring With Spock and Byteman
  • Mastering Unit Testing and Test-Driven Development in Java
  • Comprehensive Guide to Unit Testing Spring AOP Aspects

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!