1

习惯了单元测试以后,一些代码在提交前如果不测试一下总是感觉心里面空空的,没有底气可言。

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);
  }
}

潘杰
3.1k 声望238 粉丝