假设我们当前有一个 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); :point\_left:
}
而此时由于被 Mock
掉的方法声明了返回值,所以Mockito则会使用 null
来做为返回值来访问AddYzAspect
中的 afterReturnName
方法。所以此时则会发生了个 NullPointerException
异常:
java.lang.NullPointerException
at club.yunzhi.smartcommunity.aspects.AddYzAspect.afterReturnName(AddYzAspect.java:14)
所以我们在Mock被切的方法前,需要提前把切面的相关方法Mock掉,同时由于Mock被切方法时会以
null
来做为方法的返回值,所以在相应的参数上直接写入null
即可: