Spring Boot 切面的一種的測試方法

myskies發表於2021-12-02

習慣了單元測試以後,一些程式碼在提交前如果不測試一下總是感覺心裡面空空的,沒有底氣可言。

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

相關文章