springboot系列文章之使用單元測試

pjmike_pj發表於2018-09-10

前言

springboot提供了 spirng-boot-starter-test以供開發者使用單元測試,在引入 spring-boot-starter-test依賴後:

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
</dependency>
複製程式碼

其中包含以下幾個庫:

  • Junit ——常用的單元測試庫
  • Spring Test & Spring Boot Test ——對Spring應用的整合測試支援
  • AssertJ——一個斷言庫
  • Hamcrest—— 一個匹配物件的庫
  • Mockito—— 一個Java模擬框架
  • JSONassert—— 一個針對JSON的斷言庫
  • JsonPath—— 用於JSON的XPath

下面我們將從Service層和Controller層的角度來簡單介紹下單元測試

Service單元測試

在SpringBoot 2.0中,建立一個Service的單元測試,程式碼如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceImplTest {
    @Autowired
    private UserService userService;
    @Test
    public void insertUser() {
        User user = new User();
        user.setUsername("li ning");
        user.setPassword("123456");
        userService.insertUser(user);
    }
}
複製程式碼

上面的測試非常簡單,主要需要注意兩個註解: @RunWith@SpringBootTest

  • @RunWith: 該註解標籤是Junit提供的,用來說明此測試類的執行者,這裡用了SpringRunner,它實際上繼承了 SpringJUnit4ClassRunner類,而 SpringJUnit4ClassRunner這個類是一個針對Junit 執行環境的自定義擴充套件,用來標準化在Springboot環境下Junit4.x的測試用例
  • @SpringBootTest 為 springApplication建立上下文並支援SpringBoot特性

使用@SpringBootTestwebEnvironment屬性定義執行環境:

  • Mock(預設): 載入WebApplicationContext 並提供模擬的web環境 Servlet環境,使用此批註時,不會啟動嵌入式伺服器
  • RANDOM_PORT: 載入WebServerApplicationContext 並提供真實的web環境,嵌入式伺服器,監聽埠是隨機的
  • DEFINED_PORT: 載入WebServerApplicationContext並提供真實的Web環境,嵌入式伺服器啟動並監聽定義的埠(來自 application.properties或預設埠 8080)
  • NONE: 使用SpringApplication載入ApplicationContext 但不提供任何Web環境

Controller的單元測試

首先建立一個Controller,程式碼如下:

@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping("/user")
    public String userMapping(@RequestBody User user){
        userService.insertUser(user);
        return "ok";
    }
}
複製程式碼

然後建立Controller的單元測試,一般有兩種建立方法。

第一種使用模擬環境進行測試

預設情況下,@SpringBootTest 不會啟動伺服器,如果需針對此模擬環境測試Web端點,可以如下配置 MockMvc:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void userMapping() throws Exception {
        String content = "{\"username\":\"pj_mike\",\"password\":\"123456\"}";
        mockMvc.perform(MockMvcRequestBuilders.request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("ok"));
    }
}
複製程式碼

這裡有一個 @AutoConfigureMockMvc註解,該註解表示啟動測試的時候自動注入 MockMvc,而這個MockMvc有以下幾個基本的方法:

  • perform : 執行一個RequestBuilder請求,會自動執行SpringMVC的流程並對映到相應的控制器執行處理。
  • andExpect: 新增RequsetMatcher驗證規則,驗證控制器執行完成後結果是否正確
  • andDo: 新增ResultHandler結果處理器,比如除錯時列印結果到控制檯
  • andReturn: 最後返回相應的MvcResult,然後進行自定義驗證/進行下一步的非同步處理

這裡有一個小技巧,一般來說對於一個controller中往往有不止一個Request請求需要測試,敲打MockMvcRequestBuilders與MockMvcResultMatchers會顯得比較繁瑣,有一個簡便的方法就是將這兩個類的方法使用 import static靜態匯入,然後就可以直接使用兩個類的靜態方法了。然後程式碼就變成如下所示:

...
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void userMapping() throws Exception {
        String content = "{\"username\":\"pj_mike\",\"password\":\"123456\"}";
        mockMvc.perform(request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));
    }
}
複製程式碼

另外,如果是隻想關注Web層而不是啟動完整的ApplicationContext,可以考慮使用 @WebMvcTest 註解,該註解不能與@SpringBootTest搭配使用,而且它只關注Web層面,至於涉及到資料層的時候,需要引入相關依賴,關於這個註解更多的介紹請參閱官方文件: docs.spring.io/spring-boot…

使用MockMvcBuilder構建MockMvc物件

除了上面用 @AutoConfigureMockMvc 註解直接自動注入 MockMvc的方式,我們還可以利用MockMvcBuilder來構建MockMvc物件,示例程式碼如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest4 {
    @Autowired
    private WebApplicationContext web;
    private MockMvc mockMvc;

    @Before
    public void setupMockMvc() {
        mockMvc = MockMvcBuilders.webAppContextSetup(web).build();
    }
    @Test
    public void userMapping() throws Exception {
        String content = "{\"username\":\"pj_m\",\"password\":\"123456\"}";
        mockMvc.perform(request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));
    }
}
複製程式碼

第二種使用真實Web環境進行測試

在@SpringBootTest註解中設定屬性 webEnvironment = WebEnvironment.RANDOM_PORT,每次執行的時候會隨機選擇一個可用埠。我們也可以還使用 @LoalServerPort註解用於本地埠號。下面是測試程式碼:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest3 {
    @Autowired
    private TestRestTemplate testRestTemplate;
    @Test
    public void userMapping() throws Exception {
        User user = new User();
        user.setUsername("pj_pj");
        user.setPassword("123456");
        ResponseEntity<String> responseEntity = testRestTemplate.postForEntity("/user", user, String.class);
        System.out.println("Result: "+responseEntity.getBody());
        System.out.println("狀態碼: "+responseEntity.getStatusCodeValue());
    }
}
複製程式碼

上面的程式碼中有一個關鍵的類——TestRestTemplate, TestRestTemplate是Spring的RestTemplate的一種替代品,可用於整合測試,更RestTemplate的使用功能方法類似,一般用於真實web環境測試中,關於該類更加詳細的用法參考官方文件: docs.spring.io/spring-boot…

單元測試回滾

單元測試的時候,如果不想造成垃圾資料,可以開啟事務功能,在方法或類頭部新增 @Transactional註解即可,在官方文件中對此也有說明:

If your test is @Transactional, it rolls back the transaction at the end of each test method by default. However, as using this arrangement with either RANDOM_PORT or DEFINED_PORT implicitly provides a real servlet environment, the HTTP client and server run in separate threads and, thus, in separate transactions. Any transaction initiated on the server does not roll back in this case

解讀一下,在單元測試中使用 @Transactional註解,預設情況下在測試方法的末尾會回滾事務。然而有一些特殊情況需要注意,當我們使用 RANDOM_PORTDEFINED_PORT這種安排隱式提供了一個真正的Servlet環境,所以HTTP客戶端和伺服器將在不同的執行緒中執行,從而分離事務,這種情況下,在伺服器上啟動的任何事務都不會回滾。

當然如果你想關閉回滾,只要加上 @Rollback(false)註解即可,@Rollback表示事務執行完回滾,支援傳入一個value,預設true即回滾,false不回滾。

還有一種情況需要注意,就是如果你使用的資料庫是MySQL,有時候會發現加了註解 @Transactionl也不會回滾,那麼你就要檢視一下你的預設引擎是不是InnoDB,如果不是就要改成 InnoDB。

MyISAM 與 InnoDB是mysql目前比較常用的兩個資料庫引擎,MyISAM與InnoDB的主要的不同點在於效能和事務控制上,這裡簡單介紹下兩者的區別與轉換方法:

  • MyISAM: MyISAM是MySQL5.5之前版本預設的資料庫儲存引擎,MyISAM提供高速儲存和檢索,以及全文搜尋能力,適合資料倉儲等查詢頻繁的應用,但不支援事務和外來鍵,不能在表損壞後恢復資料
  • InnoDB: InnoDB是MySQL5.5版本的預設資料庫儲存引擎,InnoDB具有提交,回滾和崩潰恢復能力的事務安全,支援事務和外來鍵,比起MyISAM,InnoDB寫的處理效率差一些並且會佔用更多的磁碟空間以保留資料和索引。

如果你的資料表是MyISAM引擎,由於它不支援事務,在單元測試中新增事務註解,測試方法也是不會回滾的。

修改預設引擎

  • 檢視MySQL當前預設的儲存引擎
mysql> show variables like '%storage_engine%';
複製程式碼
  • 看具體的表user表用了什麼引擎(engine後面的就表示當前表的儲存引擎)
mysql> show create table user;
複製程式碼
  • 將user表修為InnoDB儲存引擎
mysql> ALTER TABLE user ENGINE=INNODB;

複製程式碼

注意

這裡還有一點需要注意的地方,當我們使用Spring Data JPA時,如果沒有指定MySQL建表時的儲存引擎,預設情況下會使用MySQL的MyISAM,這也是一個坑點,這種情況下,你在單元測試使用@Transactional註解,回滾不會起作用。

解決方法是將 hibernate.dialect屬性配置成hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect,指定MySQL建表的時候使用 InnoDB引擎,示例配置檔案如下:

spring:
  jpa:
    # 資料庫型別
    database: mysql
    # 輸出日誌
    show-sql: true
    properties:
      hibernate:
        # JPA配置
        hbm2ddl.auto: update
        # mysql儲存型別配置
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
複製程式碼

小結

上面簡單總結了springboot下如何使用單元測試,關於單元測試更加詳細的介紹請參閱官方文件:docs.spring.io/spring-boot…

參考資料 & 鳴謝

相關文章