前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用JUnit和Byteman测试Spring中的异步操作

用JUnit和Byteman测试Spring中的异步操作

作者头像
用户6543014
发布2020-02-12 12:18:50
1.8K0
发布2020-02-12 12:18:50
举报
文章被收录于专栏:CU技术社区CU技术社区

在本文中,我们可以找到如何在使用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:

代码语言:javascript
复制
@RestController
public class UserController {
   @Autowired
   private UserService service;
   @ResponseBody
   @PostMapping("/users")
   public UserDto post(@RequestBody UserDto dto)
   {
       return service.registerUser(dto);

   }
}

处理“用户”对象的服务:

代码语言:javascript
复制
@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;
   }
}

处理邮件的服务:

代码语言:javascript
复制
@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);
   }
}

让我们去测试代码:

代码语言:javascript
复制
@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 = "szymon.doe@nosuch.domain.com";
       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”创建一个组件。

代码语言:javascript
复制
@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类型的某些方法。

代码语言:javascript
复制
@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();
}

应用这些更改后,我们可以实现以下测试类:

代码语言:javascript
复制
@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 = "szymon.doe@nosuch.domain.com";
       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即可测试相同的测试用例,但需要更改源代码。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-01-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 SACC开源架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档