SpringBoot 測試支持由兩個模塊提供:
- spring-boot-test 包含核心項目
- spring-boot-test-autoconfigure 支持測試的自動配置
通常我們只要引入 spring-boot-starter-test
依賴就行,它包含了一些常用的模塊 Junit、Spring Test、AssertJ、Hamcrest、Mockito 等。
相關注解
SpringBoot 使用了 Junit4 作為單元測試框架,所以注解與 Junit4 是一致的。
注解 | 作用 |
---|---|
@Test(excepted==xx.class,timeout=毫秒數) | 修飾一個方法為測試方法,excepted參數可以忽略某些異常類 |
@Before | 在每一個測試方法被運行前執行一次 |
@BeforeClass | 在所有測試方法執行前執行 |
@After | 在每一個測試方法運行后執行一次 |
@AfterClass | 在所有測試方法執行后執行 |
@Ignore | 修飾的類或方法會被測試運行器忽略 |
@RunWith | 更改測試運行器 |
@SpringBootTest
SpringBoot提供了一個 @SpringBootTest 注解用於測試 SpringBoot 應用,它可以用作標准 spring-test @ContextConfiguration 注釋的替代方法,其原理是通過 SpringApplication 在測試中創建ApplicationContext。
1 @RunWith(SpringRunner.class) 2 @SpringBootTest 3 public class ApplicationTest { 4 }
該注解提供了兩個屬性用於配置:
- webEnvironment:指定Web應用環境,它可以是以下值
- MOCK:提供一個模擬的 Servlet 環境,內置的 Servlet 容器沒有啟動,配合可以與@AutoConfigureMockMvc 結合使用,用於基於 MockMvc 的應用程序測試。
- RANDOM_PORT:加載一個 EmbeddedWebApplicationContext 並提供一個真正嵌入式的 Servlet 環境,隨機端口。
- DEFINED_PORT:加載一個 EmbeddedWebApplicationContext 並提供一個真正嵌入式的 Servlet 環境,默認端口 8080 或由配置文件指定。
- NONE:使用 SpringApplication 加載 ApplicationContext,但不提供任何 servlet 環境。
- classes:指定應用啟動類,通常情況下無需設置,因為 SpringBoot 會自動搜索,直到找到 @SpringBootApplication 或 @SpringBootConfiguration 注解。
單元測試回滾
如果你添加了 @Transactional 注解,它會在每個測試方法結束時會進行回滾操作。
但是如果使用 RANDOM_PORT 或 DEFINED_PORT 這種真正的 Servlet 環境,HTTP 客戶端和服務器將在不同的線程中運行,從而分離事務。 在這種情況下,在服務器上啟動的任何事務都不會回滾。
斷言
JUnit4 結合 Hamcrest 提供了一個全新的斷言語法——assertThat
,結合 Hamcrest 提供的匹配符,就可以表達全部的測試思想。
// 一般匹配符 int s = new C().add(1, 1); // allOf:所有條件必須都成立,測試才通過 assertThat(s, allOf(greaterThan(1), lessThan(3))); // anyOf:只要有一個條件成立,測試就通過 assertThat(s, anyOf(greaterThan(1), lessThan(1))); // anything:無論什么條件,測試都通過 assertThat(s, anything()); // is:變量的值等於指定值時,測試通過 assertThat(s, is(2)); // not:和is相反,變量的值不等於指定值時,測試通過 assertThat(s, not(1)); // 數值匹配符 double d = new C().div(10, 3); // closeTo:浮點型變量的值在3.0±0.5范圍內,測試通過 assertThat(d, closeTo(3.0, 0.5)); // greaterThan:變量的值大於指定值時,測試通過 assertThat(d, greaterThan(3.0)); // lessThan:變量的值小於指定值時,測試通過 assertThat(d, lessThan(3.5)); // greaterThanOrEuqalTo:變量的值大於等於指定值時,測試通過 assertThat(d, greaterThanOrEqualTo(3.3)); // lessThanOrEqualTo:變量的值小於等於指定值時,測試通過 assertThat(d, lessThanOrEqualTo(3.4)); // 字符串匹配符 String n = new C().getName("Magci"); // containsString:字符串變量中包含指定字符串時,測試通過 assertThat(n, containsString("ci")); // startsWith:字符串變量以指定字符串開頭時,測試通過 assertThat(n, startsWith("Ma")); // endsWith:字符串變量以指定字符串結尾時,測試通過 assertThat(n, endsWith("i")); // euqalTo:字符串變量等於指定字符串時,測試通過 assertThat(n, equalTo("Magci")); // equalToIgnoringCase:字符串變量在忽略大小寫的情況下等於指定字符串時,測試通過 assertThat(n, equalToIgnoringCase("magci")); // equalToIgnoringWhiteSpace:字符串變量在忽略頭尾任意空格的情況下等於指定字符串時,測試通過 assertThat(n, equalToIgnoringWhiteSpace(" Magci ")); // 集合匹配符 List<String> l = new C().getList("Magci"); // hasItem:Iterable變量中含有指定元素時,測試通過 assertThat(l, hasItem("Magci")); Map<String, String> m = new C().getMap("mgc", "Magci"); // hasEntry:Map變量中含有指定鍵值對時,測試通過 assertThat(m, hasEntry("mgc", "Magci")); // hasKey:Map變量中含有指定鍵時,測試通過 assertThat(m, hasKey("mgc")); // hasValue:Map變量中含有指定值時,測試通過 assertThat(m, hasValue("Magci"))
基本的單元測試例子
下面是一個基本的單元測試例子,對某個方法的返回結果進行斷言:
1 @Service 2 public class UserService { 3 4 public String getName() { 5 return "lyTongXue"; 6 } 7 8 }
1 @RunWith(SpringRunner.class) 2 @SpringBootTest 3 public class UserServiceTest { 4 5 @Autowired 6 private UserService service; 7 8 @Test 9 public void getName() { 10 String name = service.getName(); 11 assertThat(name,is("lyTongXue")); 12 } 13 14 }
Controller 測試
Spring 提供了 MockMVC 用於支持 RESTful 風格的 Spring MVC 測試,使用 MockMvcBuilder 來構造MockMvc 實例。MockMvc 有兩個實現:
-
StandaloneMockMvcBuilder:指定 WebApplicationContext,它將會從該上下文獲取相應的控制器並得到相應的 MockMvc
1 @RunWith(SpringRunner.class) 2 @SpringBootTest 3 public class UserControllerTest { 4 @Autowired 5 private WebApplicationContext webApplicationContext; 6 private MockMvc mockMvc; 7 @Before 8 public void setUp() throws Exception { 9 mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 10 }
-
DefaultMockMvcBuilder:通過參數指定一組控制器,這樣就不需要從上下文獲取了
1 @RunWith(SpringRunner.class) 2 public class UserControllerTest { 3 private MockMvc mockMvc; 4 @Before 5 public void setUp() throws Exception { 6 mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build(); 7 } 8 }
下面是一個簡單的用例,對 UserController 的 /v1/users/{id}
接口進行測試。
1 @RestController 2 @RequestMapping("v1/users") 3 public class UserController { 4 5 @GetMapping("/{id}") 6 public User get(@PathVariable("id") String id) { 7 return new User(1, "lyTongXue"); 8 } 9 10 @Data 11 @AllArgsConstructor 12 public class User { 13 private Integer id; 14 private String name; 15 } 16 17 }
1 // ... 2 import static org.hamcrest.Matchers.containsString; 3 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 4 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 5 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 7 @RunWith(SpringRunner.class) 8 @SpringBootTest 9 public class UserControllerTest { 10 11 @Autowired 12 private WebApplicationContext webApplicationContext; 13 private MockMvc mockMvc; 14 15 @Before 16 public void setUp() { 17 mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 18 } 19 20 @Test 21 public void getUser() { 22 mockMvc.perform(get("/v1/users/1") 23 .accept(MediaType.APPLICATION_JSON_UTF8)) 24 .andExpect(status().isOk()) 25 .andExpect(content().string(containsString("\"name\":\"lyTongXue\""))); 26 } 27 28 }
方法描述
-
perform:執行一個 RequestBuilder 請求,返回一個 ResultActions 實例對象,可對請求結果進行期望與其它操作
-
get:聲明發送一個 get 請求的方法,更多的請求類型可查閱→MockMvcRequestBuilders 文檔
-
andExpect:添加 ResultMatcher 驗證規則,驗證請求結果是否正確,驗證規則可查閱→MockMvcResultMatchers 文檔
-
andDo:添加 ResultHandler 結果處理器,比如調試時打印結果到控制台,更多處理器可查閱→MockMvcResultHandlers 文檔
-
andReturn:返回執行請求的結果,該結果是一個恩 MvcResult 實例對象→MvcResult 文檔
Mock 數據
在單元測試中,Service 層的調用往往涉及到對數據庫、中間件等外部依賴。而在單元測試 AIR 原則中,單元測試應該是可以重復執行的,不應受到外界環境的影響的。此時我們可以通過 Mock 一個實現來處理這種情況。
如果不需要對靜態方法,私有方法等特殊進行驗證測試,則僅僅使用 Spring boot 自帶的 Mockito 即可完成相關的測試數據 Mock。若需要則可以使用 PowerMock,簡單實用,結合 Spring 可以使用注解注入。
@MockBean
SpringBoot 在執行單元測試時,會將該注解的 Bean 替換掉 IOC 容器中原生 Bean。
例如下面代碼中, ProjectService 中通過 ProjectMapper 的 selectById 方法進行數據庫查詢操作:
1 @Service 2 public class ProjectService { 3 4 @Autowired 5 private ProjectMapper mapper; 6 7 public ProjectDO detail(String id) { 8 return mapper.selectById(id); 9 } 10 11 }
此時我們可以對 Mock 一個 ProjectMapper 對象替換掉 IOC 容器中原生的 Bean,來模擬數據庫查詢操作,如:
1 @RunWith(SpringRunner.class) 2 @SpringBootTest 3 public class ProjectServiceTest { 4 5 @MockBean 6 private ProjectMapper mapper; 7 @Autowired 8 private ProjectService service; 9 10 @Test 11 public void detail() { 12 ProjectDemoDO model = new ProjectDemoDO(); 13 model.setId("1"); 14 model.setName("dubbo-demo"); 15 Mockito.when(mapper.selectById("1")).thenReturn(model); 16 ProjectDemoDO entity = service.detail("1"); 17 assertThat(entity.getName(), containsString("dubbo-demo")); 18 } 19 20 }
Mockito 常用方法
Mockito 更多的使用可查看→官方文檔
mock() 對象
1 List list = mock(List.class);
verify() 驗證互動行為
1 @Test 2 public void mockTest() { 3 List list = mock(List.class); 4 list.add(1); 5 // 驗證 add(1) 互動行為是否發生 6 Mockito.verify(list).add(1); 7 }
when() 模擬期望結果
1 @Test 2 public void mockTest() { 3 List list = mock(List.class); 4 when(mock.get(0)).thenReturn("hello"); 5 assertThat(mock.get(0),is("hello")); 6 }
doThrow() 模擬拋出異常
1 @Test(expected = RuntimeException.class) 2 public void mockTest(){ 3 List list = mock(List.class); 4 doThrow(new RuntimeException()).when(list).add(1); 5 list.add(1); 6 }
@Mock 注解
在上面的測試中我們在每個測試方法里都 mock
了一個 List 對象,為了避免重復的 mock
,使測試類更具有可讀性,我們可以使用下面的注解方式來快速模擬對象:
1 // @RunWith(MockitoJUnitRunner.class) 2 public class MockitoTest { 3 @Mock 4 private List list; 5 6 public MockitoTest(){ 7 // 初始化 @Mock 注解 8 MockitoAnnotations.initMocks(this); 9 } 10 11 @Test 12 public void shorthand(){ 13 list.add(1); 14 verify(list).add(1); 15 } 16 }
when() 參數匹配
1 @Test 2 public void mockTest(){ 3 Comparable comparable = mock(Comparable.class); 4 //預設根據不同的參數返回不同的結果 5 when(comparable.compareTo("Test")).thenReturn(1); 6 when(comparable.compareTo("Omg")).thenReturn(2); 7 assertThat(comparable.compareTo("Test"),is(1)); 8 assertThat(comparable.compareTo("Omg"),is(2)); 9 //對於沒有預設的情況會返回默認值 10 assertThat(list.get(1),is(999)); 11 assertThat(comparable.compareTo("Not stub"),is(0)); 12 }
Answer 修改對未預設的調用返回默認期望
1 @Test 2 public void mockTest(){ 3 //mock對象使用Answer來對未預設的調用返回默認期望值 4 List list = mock(List.class,new Answer() { 5 @Override 6 public Object answer(InvocationOnMock invocation) throws Throwable { 7 return 999; 8 } 9 }); 10 //下面的get(1)沒有預設,通常情況下會返回NULL,但是使用了Answer改變了默認期望值 11 assertThat(list.get(1),is(999)); 12 //下面的size()沒有預設,通常情況下會返回0,但是使用了Answer改變了默認期望值 13 assertThat(list.size(),is(999)); 14 }
spy() 監控真實對象
Mock 不是真實的對象,它只是創建了一個虛擬對象,並可以設置對象行為。而 Spy是一個真實的對象,但它可以設置對象行為。
1 @Test(expected = IndexOutOfBoundsException.class) 2 public void mockTest(){ 3 List list = new LinkedList(); 4 List spy = spy(list); 5 //下面預設的spy.get(0)會報錯,因為會調用真實對象的get(0),所以會拋出越界異常 6 when(spy.get(0)).thenReturn(3); 7 //使用doReturn-when可以避免when-thenReturn調用真實對象api 8 doReturn(999).when(spy).get(999); 9 //預設size()期望值 10 when(spy.size()).thenReturn(100); 11 //調用真實對象的api 12 spy.add(1); 13 spy.add(2); 14 assertThat(spy.size(),is(100)); 15 assertThat(spy.size(),is(1)); 16 assertThat(spy.size(),is(2)); 17 verify(spy).add(1); 18 verify(spy).add(2); 19 assertThat(spy.get(999),is(999)); 20 }
reset() 重置 mock
1 @Test 2 public void reset_mock(){ 3 List list = mock(List.class); 4 when(list.size()).thenReturn(10); 5 list.add(1); 6 assertThat(list.size(),is(10)); 7 //重置mock,清除所有的互動和預設 8 reset(list); 9 assertThat(list.size(),is(0)); 10 }
times() 驗證調用次數
1 @Test 2 public void verifying_number_of_invocations(){ 3 List list = mock(List.class); 4 list.add(1); 5 list.add(2); 6 list.add(2); 7 list.add(3); 8 list.add(3); 9 list.add(3); 10 //驗證是否被調用一次,等效於下面的times(1) 11 verify(list).add(1); 12 verify(list,times(1)).add(1); 13 //驗證是否被調用2次 14 verify(list,times(2)).add(2); 15 //驗證是否被調用3次 16 verify(list,times(3)).add(3); 17 //驗證是否從未被調用過 18 verify(list,never()).add(4); 19 //驗證至少調用一次 20 verify(list,atLeastOnce()).add(1); 21 //驗證至少調用2次 22 verify(list,atLeast(2)).add(2); 23 //驗證至多調用3次 24 verify(list,atMost(3)).add(3); 25 }
inOrder() 驗證執行順序
1 @Test 2 public void verification_in_order(){ 3 List list = mock(List.class); 4 List list2 = mock(List.class); 5 list.add(1); 6 list2.add("hello"); 7 list.add(2); 8 list2.add("world"); 9 //將需要排序的mock對象放入InOrder 10 InOrder inOrder = inOrder(list,list2); 11 //下面的代碼不能顛倒順序,驗證執行順序 12 inOrder.verify(list).add(1); 13 inOrder.verify(list2).add("hello"); 14 inOrder.verify(list).add(2); 15 inOrder.verify(list2).add("world"); 16 }
verifyZeroInteractions() 驗證零互動行為
1 @Test 2 public void mockTest(){ 3 List list = mock(List.class); 4 List list2 = mock(List.class); 5 List list3 = mock(List.class); 6 list.add(1); 7 verify(list).add(1); 8 verify(list,never()).add(2); 9 //驗證零互動行為 10 verifyZeroInteractions(list2,list3); 11 }
verifyNoMoreInteractions() 驗證冗余互動行為
1 @Test(expected = NoInteractionsWanted.class) 2 public void mockTest(){ 3 List list = mock(List.class); 4 list.add(1); 5 list.add(2); 6 verify(list,times(2)).add(anyInt()); 7 //檢查是否有未被驗證的互動行為,因為add(1)和add(2)都會被上面的anyInt()驗證到,所以下面的代碼會通過 8 verifyNoMoreInteractions(list); 9 10 List list2 = mock(List.class); 11 list2.add(1); 12 list2.add(2); 13 verify(list2).add(1); 14 //檢查是否有未被驗證的互動行為,因為add(2)沒有被驗證,所以下面的代碼會失敗拋出異常 15 verifyNoMoreInteractions(list2); 16 }