习惯了单元测试以后,一些代码在提交前如果不测试一下总是感觉心里面空空的,没有底气可言。
Spring Boot提供的官方注释结合强大的Mockito能够解决大部分在测试方面的需求。但貌似对于代理模式下的切面却并不如意。
情景模拟
假设我们当前有一个StudentControllor
,该控制器中存一个getNameById
方法。
@RestController
public class StudentController {
@GetMapping("{id}")
public Student getNameById(@PathVariable Long id) {
return new Student("测试姓名");
}
public static class Student {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
在没有切面前,我们访问该方法将得到相应带有测试姓名的学生信息。
建立切面
现在,我们使用切面的方法在返回的名字后台追加一个Yz
后缀。
@Aspect
@Component
public class AddYzAspect {
@AfterReturning(value = "execution(* club.yunzhi.smartcommunity.controller.StudentController.getNameById(..))",
returning = "student")
public void afterReturnName(StudentController.Student student) {
student.setName(student.getName() + "Yz");
}
}
测试
如果我们使用普通测试的方法来直接断言返回的姓名当然是可行的:
@SpringBootTest
class AddYzAspectTest {
@Autowired
StudentController studentController;
@Test
void afterReturnName() {
Assertions.assertEquals(studentController.getNameById(123L).getName(), "测试姓名Yz");
}
}
但往往切面中的逻辑并非这么简单,在实际的测试中其实我们也完成没有必要关心在切面中到底发生了什么(发生了什么应该在测试切面的方法中完成)。我们在此主要关心的是切面是否成功的被执行了,同时建立相应的断言,以防止在日后面的代码迭代过程中不小心使当前的切面失效。
MockBean
Spring Boot为我们提供了MockBean
来直接Mock
掉某个Bean
。在测试切面是否成功执行时,我们并不关心StudentController
中的getNameById()
方法的执行逻辑,所以适用于合适MockBean
来声明。
@SpringBootTest
class AddYzAspectTest {
- @Autowired
+ @MockBean
StudentController studentController;
但MockBean
并不适合于测试切面,这是由于MockBean
在生成新的代理时将直接忽略掉相关切面的注解,导致切面直接失效。
同时MockBean
虽然可以用于来模拟Controller
,但如果用它来模拟Aspect则会发生错误。
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration': BeanPostProcessor before instantiation of bean failed;
MockSpy
除了MockBean
以外,Spring Boot还准备了携带了真正的Bean
,但该Bean
又可以随时按需求Mock
掉的,同时使用该注解生成的Bean
并不会破坏原来的切面。
class AddYzAspectTest {
@SpyBean
StudentController studentController;
@SpyBean
AddYzAspect addYzAspect;
但在这需要注意的@SpyBean
虽然成功的生成了两个可以被Mock
掉的Bean
,但在执行相应的Mock
方法时其对应的切面方法会自动调用一次。比如以下代码将自动调用AddYzAspect
中的afterReturnName
方法。
@Test
void afterReturnName() {
StudentController.Student student = new StudentController.Student("test");
Mockito.doReturn(student).when(this.studentController).getNameById(123L);
}
而此时由于被Mock
掉的方法声明了返回值,所以Mockito则会使用null
来做为返回值来访问AddYzAspect
中的afterReturnName
方法。所以此时则会发生了个NullPointerException
异常:
java.lang.NullPointerException
at club.yunzhi.smartcommunity.aspects.AddYzAspect.afterReturnName(AddYzAspect.java:14)
所以我们在Mock被切的方法前,需要提前把切面的相关方法Mock掉,同时由于Mock被切方法时会以null
来做为方法的返回值,所以在相应的参数上直接写入null
即可:
@Test
void afterReturnName() {
Mockito.doNothing().when(this.addYzAspect).afterReturnName(null);
Mockito.doReturn(null).when(this.studentController).getNameById(123L);
完整测试代码
@SpringBootTest
class AddYzAspectTest {
@SpyBean
StudentController studentController;
@SpyBean
AddYzAspect addYzAspect;
@Test
void afterReturnName() {
Mockito.doNothing().when(this.addYzAspect).afterReturnName(null);
Mockito.doReturn(null).when(this.studentController).getNameById(123L);
Mockito.verify(this.addYzAspect, Mockito.times(1)).afterReturnName(null);
}
}