How to Test Asynchronous Spring Operations with Byteman and BMUnit

This guide shows how to test asynchronous email‑sending logic in a Spring Boot application using Byteman, the BMUnit extension, JUnit 4, and a join‑er mechanism, all without modifying production code.

ITPUB
ITPUB
ITPUB
How to Test Asynchronous Spring Operations with Byteman and BMUnit

Testing asynchronous email sending in a Spring Boot application

This example shows how to verify that a user‑registration flow triggers an asynchronous email‑sending step without modifying the production code.

Key tools

Byteman – injects Java code into methods at runtime.

BMUnit – integrates Byteman with JUnit 4 (and TestNG) via a rule.

bmunit‑extension – provides a BMUnitMethodRule and helper utilities for JUnit 4 tests. Source code: https://github.com/starnowski/bmunit-extension/tree/feature/article_examples

Demo application

The application exposes a /users REST endpoint that registers a new user and sends a verification email asynchronously.

@RestController
public class UserController {
    @Autowired
    private UserService service;

    @PostMapping("/users")
    public UserDto post(@RequestBody UserDto dto) {
        return service.registerUser(dto);
    }
}
@Service
public class UserService {
    @Autowired private PasswordEncoder passwordEncoder;
    @Autowired private RandomHashGenerator randomHashGenerator;
    @Autowired private MailService mailService;
    @Autowired private UserRepository repository;

    @Transactional
    public UserDto registerUser(UserDto dto) {
        User user = new User()
            .setEmail(dto.getEmail())
            .setPassword(passwordEncoder.encode(dto.getPassword()))
            .setEmailVerificationHash(randomHashGenerator.compute());
        user = repository.save(user);
        UserDto response = new UserDto()
            .setId(user.getId())
            .setEmail(user.getEmail());
        mailService.sendMessageToNewUser(response, user.getEmailVerificationHash());
        return response;
    }
}
@Service
public class MailService {
    @Autowired private MailMessageRepository mailMessageRepository;
    @Autowired private JavaMailSender emailSender;
    @Autowired private ApplicationEventPublisher applicationEventPublisher;

    @Transactional
    public void sendMessageToNewUser(UserDto dto, String emailVerificationHash) {
        MailMessage mailMessage = new MailMessage();
        mailMessage.setMailSubject("New user");
        mailMessage.setMailTo(dto.getEmail());
        mailMessage.setMailContent(emailVerificationHash);
        mailMessageRepository.save(mailMessage);
        applicationEventPublisher.publishEvent(new NewUserEvent(mailMessage));
    }

    @Async
    @TransactionalEventListener
    public void handleNewUserEvent(NewUserEvent newUserEvent) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(newUserEvent.getMailMessage().getMailTo());
        message.setSubject(newUserEvent.getMailMessage().getMailSubject());
        message.setText(newUserEvent.getMailMessage().getMailContent());
        emailSender.send(message);
    }
}

Testing with BMUnit

The test runs under JUnit 4, loads Byteman rules with BMUnitMethodRule, and captures outgoing mail with GreenMailRule. A “joiner” mechanism synchronises the test thread with the asynchronous mail‑sending thread.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Sql(value = CLEAR_DATABASE_SCRIPT_PATH, config = @SqlConfig(transactionMode = ISOLATED), executionPhase = BEFORE_TEST_METHOD)
@Sql(value = CLEAR_DATABASE_SCRIPT_PATH, config = @SqlConfig(transactionMode = ISOLATED), executionPhase = AFTER_TEST_METHOD)
@EnableAsync
public class UserControllerTest {
    @Rule public BMUnitMethodRule bmUnitMethodRule = new BMUnitMethodRule();
    @Rule public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP_IMAP);
    @Autowired UserRepository userRepository;
    @Autowired TestRestTemplate restTemplate;
    @LocalServerPort private int port;

    @Test
    @BMUnitConfig(verbose = true, bmunitVerbose = true)
    @BMRules(rules = {
        @BMRule(name = "signal thread waiting for mutex \"UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation\"",
               targetClass = "com.github.starnowski.bmunit.extension.junit4.spock.spring.demo.services.MailService",
               targetMethod = "handleNewUserEvent(com.github.starnowski.bmunit.extension.junit4.spock.spring.demo.util.NewUserEvent)",
               targetLocation = "AT EXIT",
               action = "joinEnlist(\"UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation\")")
    })
    public void shouldCreateNewUserAndSendMailMessageInAsyncOperation() throws Exception {
        // given
        String expectedEmail = "[email protected]";
        assertThat(userRepository.findByEmail(expectedEmail)).isNull();
        UserDto dto = new UserDto().setEmail(expectedEmail).setPassword("XXX");
        createJoin("UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation", 1);
        assertEquals(0, greenMail.getReceivedMessages().length);

        // when
        UserDto response = restTemplate.postForObject(new URI("http://localhost:" + port + "/users"), dto, UserDto.class);
        joinWait("UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation", 1, 15000);

        // then
        assertThat(userRepository.findByEmail(expectedEmail)).isNotNull();
        assertThat(greenMail.getReceivedMessages().length).isEqualTo(1);
        assertThat(greenMail.getReceivedMessages()[0].getSubject()).contains("New user");
        assertThat(greenMail.getReceivedMessages()[0].getAllRecipients()[0].toString()).contains(expectedEmail);
    }
}

The helper methods work as follows: createJoin(key, count) registers a join point identified by key and expects count threads to reach it.

The @BMRule injects a Byteman action at the exit of MailService.handleNewUserEvent that calls joinEnlist(key), signalling the test thread. joinWait(key, count, timeout) blocks the test until the expected number of joins is observed or the timeout expires.

Alternative approach using CountDownLatch

If Byteman is not available, a similar synchronisation can be achieved by adding a latch component to the production code.

@Component
public class DummyApplicationCountDownLatch implements IApplicationCountDownLatch {
    private CountDownLatch mailServiceCountDownLatch;

    @Override
    public void mailServiceExecuteCountDownInHandleNewUserEventMethod() {
        if (mailServiceCountDownLatch != null) {
            mailServiceCountDownLatch.countDown();
        }
    }

    @Override
    public void mailServiceWaitForCountDownLatchInHandleNewUserEventMethod(int milliseconds) throws InterruptedException {
        if (mailServiceCountDownLatch != null) {
            mailServiceCountDownLatch.await(milliseconds, TimeUnit.MILLISECONDS);
        }
    }

    @Override
    public void mailServiceResetCountDownLatchForHandleNewUserEventMethod() {
        mailServiceCountDownLatch = new CountDownLatch(1);
    }

    @Override
    public void mailServiceClearCountDownLatchForHandleNewUserEventMethod() {
        mailServiceCountDownLatch = null;
    }
}

The MailService is modified to autowire the latch and invoke the countdown after the email is sent:

@Autowired private IApplicationCountDownLatch applicationCountDownLatch;

@Async
@TransactionalEventListener
public void handleNewUserEvent(NewUserEvent newUserEvent) {
    SimpleMailMessage message = new SimpleMailMessage();
    message.setTo(newUserEvent.getMailMessage().getMailTo());
    message.setSubject(newUserEvent.getMailMessage().getMailSubject());
    message.setText(newUserEvent.getMailMessage().getMailContent());
    emailSender.send(message);
    applicationCountDownLatch.mailServiceExecuteCountDownInHandleNewUserEventMethod();
}

The test then uses the latch instead of the Byteman joiner:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableAsync
public class UserControllerTest {
    @Rule public final GreenMailRule greenMail = new GreenMailRule(ServerSetupTest.SMTP_IMAP);
    @Autowired UserRepository userRepository;
    @Autowired TestRestTemplate restTemplate;
    @LocalServerPort private int port;
    @Autowired private IApplicationCountDownLatch applicationCountDownLatch;

    @After
    public void tearDown() {
        applicationCountDownLatch.mailServiceClearCountDownLatchForHandleNewUserEventMethod();
    }

    @Test
    public void shouldCreateNewUserAndSendMailMessageInAsyncOperation() throws Exception {
        String expectedEmail = "[email protected]";
        assertThat(userRepository.findByEmail(expectedEmail)).isNull();
        UserDto dto = new UserDto().setEmail(expectedEmail).setPassword("XXX");
        applicationCountDownLatch.mailServiceResetCountDownLatchForHandleNewUserEventMethod();
        assertEquals(0, greenMail.getReceivedMessages().length);

        UserDto response = restTemplate.postForObject(new URI("http://localhost:" + port + "/users"), dto, UserDto.class);
        applicationCountDownLatch.mailServiceWaitForCountDownLatchInHandleNewUserEventMethod(15000);

        assertThat(userRepository.findByEmail(expectedEmail)).isNotNull();
        assertThat(greenMail.getReceivedMessages().length).isEqualTo(1);
        assertThat(greenMail.getReceivedMessages()[0].getSubject()).contains("New user");
        assertThat(greenMail.getReceivedMessages()[0].getAllRecipients()[0].toString()).contains(expectedEmail);
    }
}

Conclusion

Byteman together with BMUnit enables verification of asynchronous behavior in a Spring application without any changes to the production code. The same verification can be performed with a plain Java CountDownLatch, but that approach requires adding latch‑related code to the application.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JUnitasync-testingBytemanbmunit
ITPUB
Written by

ITPUB

Official ITPUB account sharing technical insights, community news, and exciting events.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.