習慣了單元測試以後,一些程式碼在提交前如果不測試一下總是感覺心裡面空空的,沒有底氣可言。
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);
}
}