用JUnit和Byteman測試Spring中的異步操作
- 2020 年 2 月 12 日
- 筆記

在本文中,我們可以找到如何在使用spring上下文的應用程序中測試此類操作(啟用異步操作)。我們無需更改生產代碼即可實現這一目標。
測試將在JUnit 4中運行。對於測試,我們將使用Byteman庫中的功能。我們還必須附加「 Bmunit-extension」庫,該庫提供了包含JUnit規則和在測試期間使用的一些輔助方法。
Byteman是一種工具,可將Java代碼注入您的應用程序方法或Java運行時方法,而無需您重新編譯、重新打包甚至重新部署應用程序。BMUnit是一個軟件包,通過將Byteman集成到兩個最受歡迎的Java測試框架(JUnit和TestNG)中,可以很容易地將Byteman用作測試工具。
Bmunit-extension是GitHub上的一個小項目,其中包含junit4規則,該規則允許與Byteman框架集成並在JUnit和Spock測試中使用它。它包含一些輔助方法。
在本文中,我們將使用演示應用程序中的代碼,該應用程序是「 Bmunit-extension」項目的一部分。可以在https://github.com/starnowski/bmunit-extension/tree/feature/article_examples上找到源代碼。
測試用例假設我們註冊了一個新的應用程序用戶(所有事務都已提交)並向他發送電子郵件。電子郵件發送操作是異步的。
現在,該應用程序只包含一些測試,這些測試顯示了如何測試這種情況。
沒有跡象表明在演示應用程序中為Bmunit-extension實施的代碼是唯一的方法,甚至是最好的方法。該項目的主要目的是展示如何通過使用Byteman庫來對這種情況進行測試而無需更改任何Byteman。
在示例測試中,我們想檢查一個新應用程序用戶註冊流程。假設該應用程序允許通過Rest API註冊用戶。因此,Rest API客戶端發送帶有用戶數據的請求,Rest API控制器正在處理該請求。在數據庫提交事務之後,但在返回Rest API響應之前,控制器將調用異步執行器向一個具有註冊鏈接的用戶發送電子郵件(以確認電子郵件地址)。
整個過程在下面的序列圖中顯示。

現在,我猜測這可能不是註冊用戶的最佳方法。可能更好的方法是使用某種調度程序組件來檢查是否有電子郵件要發送。更不用說對於更大的應用程序,單獨的微服務將更適合。假設對於可用線程沒有問題的應用程序來說是可以的。
實現包含Rest Controller:
@RestController public class UserController { @Autowired private UserService service; @ResponseBody @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); } }
讓我們去測試代碼:
@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 IOException, URISyntaxException, MessagingException { // 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 responseEntity = restTemplate.postForObject(new URI("http://localhost:" + port + "/users"), (Object) 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); } }
測試類需要包含「 BMUnitMethodRule」類型的對象以加載Byteman規則。
BMRule批註是BMUnit項目的一部分。所有選項「name」,「 targetClass」,「 targetMethod」,「 targetLocation」和「 action」均指Byteman規則語言部分中的特定部分。選項「 targetClass」,「 targetMethod」和「 targetLocation」用於Java代碼中的指定點,然後執行規則。
「操作」選項定義到達規則點後應執行的操作。
如果您想進一步了解Byteman規則語言,請查閱《程序員指南》。
此測試方法的目的是確認可以通過rest API控制器註冊新的應用程序用戶,並且該應用程序向用戶發送包含註冊細節的詳細信息的電子郵件。最後一件重要的事情是,測試確認觸發了觸發發送電子郵件的異步執行器的方法。
為此,我們需要使用「 Joiner」機制。從Byteman的「開發人員指南」中,我們發現,在需要確保一個線程直到退出一個或多個相關線程之前不會繼續運行的情況下,聯接器很有用。
通常,在創建連接器時,我們需要指定需要連接的線程的標識和編號。在「給定」部分中,我們執行「 BMUnitUtils#createJoin(Object,int)」以創建「 UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation」連接器,其中連接器數為預期的線程數。我們希望負責發送的線程將加入。
為此,我們需要通過BMRule注釋集,在方法退出後(值「 AT EXIT」的「 targetLocation」選項),需要執行執行「 Helper#joinEnlist(Object key)」方法的某些動作,該方法不會掛起調用它的當前線程。
在執行testes方法的「when」中,調用「 BMUnitUtils#joinWait(Object,int,long)」掛起測試線程,以等待連接器「 UserControllerTest.shouldCreateNewUserAndSendMailMessageInAsyncOperation」的連接線程數達到預期值。如果預計的連接線程數不會達到預期,則執行將達到超時,並拋出某些異常。
在「then」部分中,我們檢查是否已創建用戶以及是否發送了包含正確內容的電子郵件。
感謝Byteman,可以在不更改源代碼的情況下完成此測試。
這也可以使用基本的Java機制來完成,但也需要更改源代碼。
首先,我們必須使用「 CountDownLatch」創建一個組件。
@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; } }
「 MailService」中還將需要進行一些更改,以便執行DummyApplicationCountDownLatch類型的某些方法。
@Autowired private IApplicationCountDownLatch applicationCountDownLatch; @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); applicationCountDownLatch.mailServiceExecuteCountDownInHandleNewUserEventMethod(); }
應用這些更改後,我們可以實現以下測試類:
@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 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 IOException, URISyntaxException, MessagingException, InterruptedException { // given String expectedEmail = "[email protected]"; assertThat(userRepository.findByEmail(expectedEmail)).isNull(); UserDto dto = new UserDto().setEmail(expectedEmail).setPassword("XXX"); applicationCountDownLatch.mailServiceResetCountDownLatchForHandleNewUserEventMethod(); assertEquals(0, greenMail.getReceivedMessages().length); // when UserDto responseEntity = restTemplate.postForObject(new URI("http://localhost:" + port + "/users"), (Object) dto, UserDto.class); applicationCountDownLatch.mailServiceWaitForCountDownLatchInHandleNewUserEventMethod(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); } }
結束語,Byteman允許在不更改其源代碼的情況下測試應用程序中的異步操作。無需Byteman即可測試相同的測試用例,但需要更改源代碼。