在本文中,我们可以找到如何在使用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即可测试相同的测试用例,但需要更改源代码。