专栏首页CU技术社区用JUnit和Byteman测试Spring中的异步操作

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

在本文中,我们可以找到如何在使用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 = "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”创建一个组件。

@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 = "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即可测试相同的测试用例,但需要更改源代码。

本文分享自微信公众号 - CU技术社区(ChinaUnix2013),作者:栈栈

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-01-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 深度复盘GitHub发展史:如何在短短10年内改变了人们的编程方式?

    前不久,微软以75亿美元的价格收购GitHub,引发了科技行业的关注。在短短的10年内,GitHub 改变了人们的编程方式。不仅让编程变得更简单,还改变了软件开...

    用户6543014
  • VMware 侵权 Linux 源码案原告放弃上诉:没意义

    还记得多年前那场 VMware 源码侵权案吗?在诉讼请求被法院一次次驳回后,最近原告著名 Linux 开发者 Christopher Helwig 已经决定不再...

    用户6543014
  • 可以分叉了!GitHub 上线中文帮助文档了!

    GitHub 是世界上最大的开源平台,随着它在国内的不断推广普及,不少开发者都开始纷纷采用 GitHub 来作为公司内部的代码管理工具。

    用户6543014
  • SpringCloud技术指南系列(十)配置管理之自建配置中心

    详细可以查看《SpringBoot入门建站全系列(二十三)配置文件优先级及常用配置方式》.

    品茗IT
  • 配置文件加载位置

    spring boot 启动会扫描以下位置的application.properties或者appliation.yml文件作为Spring Boot的默认配置...

    桑鱼
  • Android常用库源码解析

    glide源码 一般看源码先看他的使用方法,通过使用的方法看对应的代码。 Glide.with(MainActivity.this).load(url).i...

    六月的雨
  • SpringBoot源码学习系列之启动原理简介

    本博客通过debug方式简单跟一下Springboot application启动的源码,Springboot的启动源码是比较复杂的,本博客只是简单梳理一下源码...

    SmileNicky
  • 聊聊storm的AssignmentDistributionService

    本文主要研究一下storm的AssignmentDistributionService

    codecraft
  • [先行者课程] -- 用js实现倒计时功能的业务逻辑

    今天是2017年3月19号,周日,我们一起来学习“倒计时”这个前端“需求”。 一,看一下上周的作业,视差滚动的作业; 二,开始分享学习倒计时应用。 因为时间的关...

    web前端教室
  • Python 创建递归文件夹

    机器学习和大数据挖掘

扫码关注云+社区

领取腾讯云代金券