總所周知,測試是軟件開發中一個很是重要的環節,用來驗證程序運行是否符合預期(這個預期包括了程序的正確性、性能質量等),若是不符合預期,就根據測試的結果報告定位問題,修復問題,而後再次測試,這個過程每每須要重複屢次,直到程序的運行情況符合預期才能夠嘗試發佈、上線,不然就是對產品,軟件不負責。java
根據分類方式不一樣,測試能夠分紅不一樣的類型,通常最多見也是最重要的是根據開發階段劃分,能夠劃分出4個主要的測試類型:web
本文主要介紹的就是第一個:單元測試。做爲開發人員,其餘三個能夠不那麼熟悉,但單元測試必需要很是熟悉。spring
下面是從維基百科上摘取的單元測試的定義:sql
在計算機編程中,單元測試(英語:Unit Testing)又稱爲模塊測試, 是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工做。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。shell
能夠說單元測試的目的就是檢驗程序的正確性,對於性能、可用性等沒有要求,因此也經常說單元測試是最基本的測試,若是單元測試都沒法經過,後面的測試徹底不必進行。數據庫
Java社區中有不少第三方優秀的開源測試框架,例如JUnit,Mockito,TestNG等,下面我將介紹Junit和Mockito的使用。編程
本文不涉及軟件測試的理論知識,僅會談到測試工具的使用。json
JUnit是一款很是出名的開源測試框架,甚至不少非Java開發者都或多或少據說過。Junit如今(2018-10-15)已經發布了Junit5,多了一些特性,並且最低支持的Java版本的是Java8,但本文不打算使用Junit5,而是採用JUnit4。關於JUnit5的變化,建議到官網查看。後端
官網中提供了JUnit的jar包的下載地址,導入jar包便可使用。若是項目是Maven項目的話,也能夠往pom.xml文件里加入junit依賴,以下所示:瀏覽器
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
複製代碼
從JUnit4開始,咱們能夠在須要測試的方法上加上@Test註解來表示該方法是一個待測試的方法。在JUnit3的時候,要想測試一個方法,只能使用「命名模式」將待測試方法的方法名設置成testXXX的形式,命名模式有不少缺點和不足,因此推薦你們儘可能使用JUnit4以後的版本。下面是一個JUnit4的簡單使用案例:
public class ApplicationTest {
private int calculateSum(int a, int b) {
return a + b;
}
@Test
//這裏的方法名只是一種習慣用法,JUnit4並不強制要求必須是testXXX
public void testCalculate() {
Assert.assertEquals(10, calculateSum(5, 5)); //經過
Assert.assertEquals(10, calculateSum(20, -10)); //經過
Assert.assertEquals(10, calculateSum(0,0)); //不經過,通常不會這樣寫,這裏只是爲了演示
Assert.assertNotEquals(10, calculateSum(10, 10)); //經過
}
}
複製代碼
有@Test註解方法是待測試方法,當程序啓動的時候,會依次調用全部的待測試方法,若是在方法裏拋出異常,那麼該方法就算是測試失敗了。Assert是org.junit包下的一個類,提供了豐富的斷言API供咱們使用,例如assertEquals用來斷言期待值和實際值相等,assertNull用來斷言參數是一個null值。在案例代碼中,只有一個待測試方法,該方法的測試目標是calculateSum方法,其中的4個斷言都是爲了驗證calculateSum方法的返回值是否符合預期,啓動程序,控制檯輸出內容大體以下所示:
java.lang.AssertionError:
Expected :10
Actual :0
<Click to see difference>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:645)
at org.junit.Assert.assertEquals(Assert.java:631)
at top.yeonon.ApplicationTest.testCalculate(ApplicationTest.java:21)
.......
複製代碼
能夠看到方法拋出了一個AssertionError異常,並打印了異常堆棧,用於定位問題所在,除此以外,JUnit還給出了一個簡單的測試報告,即:
java.lang.AssertionError:
Expected :10
Actual :0
複製代碼
Expected即期待值,使咱們在程序中自定義的,Actual是calculateSum的返回值,JUnit想要告訴咱們的是:你期待的值是10,但實際值倒是0,即不符合預期,應該嘗試修復問題。
下面是一個相對比較複雜的例子(只是和上面的例子比較,實際開發中不會那麼簡單):
public class AppTest {
@Test
public void testAssertEqualAndNotEqual() {
String name = "yeonon";
Assert.assertEquals("yeonon", name);
Assert.assertNotEquals("weiyanyu", name);
}
@Test
public void testArrayEqual() {
byte[] expected = "trial".getBytes();
byte[] actual = "trial".getBytes();
Assert.assertArrayEquals("failure - byte arrays not same", expected, actual);
}
@Test
public void testBoolean() {
Assert.assertTrue(true);
Assert.assertFalse(false);
}
@Test
public void testNull() {
Assert.assertNull(null);
Assert.assertNotNull(new Object());
}
@Test
public void testThatHashItems() {
Assert.assertThat(Arrays.asList("one","two","three"), CoreMatchers.hasItems("one","two"));
}
@Test
public void testThatBoth() {
Assert.assertThat("yeonon",
CoreMatchers.both(
CoreMatchers.containsString("e"))
.and(CoreMatchers.containsString("o")));
}
}
複製代碼
其實就是試試Assert的各類API,很少說了,看看方法名字大概就知道功能了。
順便說一下,若是以爲太多的Assert和CoreMatchers看着煩,可使用靜態導入包的方式導入包,例如:
import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; 複製代碼
JUnit的使用就是那麼簡單粗暴直接,這也是爲何JUnit如此火爆的緣由之一。固然,JUnit不只僅只有那麼點功能,關於JUnit更高級的功能,建議到JUnit官網查看官方文檔,它的文檔寫的仍是不錯的。
Mockito是一款很是強大的測試框架,其最大的特色就是「Mock」,即模擬。單元測試的一個很重要的關鍵點就是儘可能在不涉及依賴關係的狀況下測試代碼,儘可能的模擬真實的環境去作測試。Mockito能夠作到這一點,他會將用到的類包裝成一個Mock對象,該Mock對象是可配置的,便可以將其行爲配置成咱們想要的樣子。
例如在一般的Web開發中,後端會分爲3層,即MVC,負責控制層的同窗可能已經把控制層寫好了,但負責模型層的同窗還沒寫好,這時候控制層的同窗想要對控制層的功能作測試,就可使用Mock模擬出一個模型層(假設接口以及定義好了,只是功能還沒實現),而後進行測試,這樣就不須要等待負責模型層的同窗寫完了。
和JUnit同樣,能夠下載jar包並導入項目,若是項目是Maven項目的話,能夠在pom.xml文件里加入以下依賴:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.21.0</version>
<scope>test</scope>
</dependency>
複製代碼
在實際用的時候,還須要加入JUnit的依賴。(但不是說mockito依賴JUnit,僅僅是項目依賴了JUnit)
下面僅介紹一個簡單例子,以下所示:
public class ApplicationTest {
//有返回值方法
public int calcSum(int a, int b) {
return 1;
}
//無返回值方法
public void noReturn() {
}
@Test
//設置單個返回值
public void testOneReturn() {
ApplicationTest test = mock(ApplicationTest.class);
when(test.calcSum(10, 10)).thenReturn(10);
assertEquals(10,test.calcSum(10, 10));
}
@Test
//設置多個返回值,按順序校驗
public void testMultiReturn() {
ApplicationTest test = mock(ApplicationTest.class);
when(test.calcSum(10, 10)).thenReturn(10).thenReturn(20);
assertEquals(10, test.calcSum(10, 10));
assertEquals(20, test.calcSum(10, 10));
}
@Test
//根據輸入參數不一樣來定義不一樣的返回值
public void testMethodParam() {
ApplicationTest test = mock(ApplicationTest.class);
when(test.calcSum(0,0)).thenReturn(1);
when(test.calcSum(1,1)).thenReturn(0);
assertEquals(1, test.calcSum(0, 0));
assertEquals(0, test.calcSum(1, 1));
}
@Test
//返回值不依賴輸入
public void testNotMethodParam() {
ApplicationTest test = mock(ApplicationTest.class);
when(test.calcSum(anyInt(),anyInt())).thenReturn(-1);
assertEquals(-1, test.calcSum(10, 10));
assertEquals(-1, test.calcSum(100, -100));
}
@Test
//根據返回值的類型來決定輸出
public void testReturnTypeOfMethodParam() {
ApplicationTest test = mock(ApplicationTest.class);
when(test.calcSum(isA(Integer.class), isA(Integer.class))).thenReturn(-100);
assertEquals(-100, test.calcSum(100, 100));
assertEquals(-100, test.calcSum(111,111));
}
@Test
//行爲驗證,主要用於驗證方法是否被調用
public void testBehavior() {
ApplicationTest test = mock(ApplicationTest.class);
test.calcSum(10, 10);
test.calcSum(10, 10);
//times(2)表示被調用兩次
verify(test, times(2)).calcSum(10, 10);
}
}
複製代碼
首先,咱們在每一個方法裏都構造了一個Mock對象,即
ApplicationTest test = mock(ApplicationTest.class);
複製代碼
構造完畢以後,就能夠作一些配置了,拿testOneReturn方法來講,使用了when(...).thenReturn(...)的方式來對mock對象進行配置,when的參數是一個方法調用,例如test.calcSum(10, 10),threnReturn的參數就是設置該方法調用的返回值。因此when(test.calcSum(10, 10)).thenReturn(10);這行代碼的意思就是「當調用test.calcSum(10,10)的時候,應該返回10」,而後調用assertEquals(10,test.calcSum(10, 10));來驗證是否正確。
這裏你可能會有點奇怪,代碼中的calcSum不管如何都應該返回-1纔對啊,那這行代碼是否能經過測試呢?答案是能!由於咱們使用when(...).thenReturn(...)就是在對這個方法調用作設置,即這裏定義的返回值是咱們自定義的,不管calcSum是如何實現的,只要咱們按照when裏規定的調用形式(例子中是test.calcSum(10, 10)),那麼就必定會返回配對的thenReturn()裏設置的值。
其餘方法就很少說了,和testOneReturn()差很少,並且也作了註釋,應該不難理解。
SpringBoot Test模塊包含了JUnit、Mockito等依賴,在對Spring Boot項目進行測試的時候,只須要添加一個Spring Boot Test的依賴便可,以下所示:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>根據官網發佈的版本進行選擇,記得避免版本衝突</version>
<scope>test</scope>
</dependency>
複製代碼
標準的Spring Boot的MVC三層代碼,我就省略了,很是簡單,直接來看測試類。
package top.yeonon.springtest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import top.yeonon.springtest.controller.UserController;
import top.yeonon.springtest.repository.UserRepository;
import top.yeonon.springtest.service.UserService;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** * @Author yeonon * @date 2018/10/15 0015 18:21 **/
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
private MockMvc mockMvc;
@Autowired
private UserController userController;
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Before
public void setUp() {
//構造mockMvc
mockMvc = MockMvcBuilders.standaloneSetup(userController, userService, userRepository).build();
}
@Test
public void testUserService() throws Exception {
RequestBuilder request = null;
//1. 註冊用戶
request = post("/users")
.param("username", "yeonon")
.param("password", "admin")
.contentType(MediaType.APPLICATION_JSON_UTF8);
mockMvc.perform(request)
.andExpect(status().is(200))
.andExpect(content().string("註冊成功")); //在業務代碼中,若是成功就會返回「註冊成功」;
//2. 根據id獲取用戶
request = get("/users/1");
mockMvc.perform(request)
.andExpect(status().is(200))
.andExpect(content().string("{\"id\":1,\"username\":\"yeonon\",\"password\":\"admin\"}"));
//3. 修改用戶信息
request = put("/users")
.param("username", "weiyanyu")
.param("password", "aaa")
.param("id", "1");
mockMvc.perform(request)
.andExpect(status().is(200))
.andExpect(content().string("更新成功"));
//4. 再次獲取信息
request = get("/users/1");
mockMvc.perform(request)
.andExpect(status().is(200))
.andExpect(content().string("{\"id\":1,\"username\":\"weiyanyu\",\"password\":\"aaa\"}"));
//5. 刪除用戶
request = delete("/users/1");
mockMvc.perform(request)
.andExpect(status().is(200))
.andExpect(content().string("刪除成功"));
}
}
複製代碼
mockMvc是Spring封裝的一個類,從名字能夠看出來是針對MVC的一個模擬,實際上也確實如此。整個測試過程能夠分爲如下幾個步驟:
這就完成了一次web測試。這裏順便說一下編碼問題,在這個測試環境下,默認的編碼方式不是UTF-8(好像是ISO-xxx,具體忘了),因此若是controller返回的有中文且不作特殊處理的話,可能會出錯。一個解決方案是,修改controller中的@RequestMapping上的produces屬性,以下所示:
@DeleteMapping(value = "{id}",produces = "application/json;charset=UTF-8")
public String deleteUser(@PathVariable("id") Long id) {
return userService.deleteUser(id);
}
複製代碼
該小測試項目中,其實用到了h2數據庫。h2是一款用Java語言開發的數據庫,可直接嵌入到應用程序中,與應用程序打包發佈,不受平臺限制,它還支持內存模式,因此很是適合用於測試環境。通常爲了方便,在測試環境使用的時候,會將項目的.sql文件載入到h2中,而後使用內存模式進行測試,在內存模式下,全部的操做都在內存中進行,不會進行持久化,因此無需擔憂會弄髒生產環境的數據庫。
spring boot對h2也有支持,咱們只須要在項目中加入h2的相關依賴並作少許配置便可使用,以下所示:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
</dependency>
複製代碼
配置以下所示:
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:h2test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.username=admin
spring.datasource.password=admin
spring.jpa.database=h2
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.path=/console
複製代碼
啓動項目以後,在瀏覽器裏輸入url進入h2控制檯,以下所示:
作好配置以後,輸入用戶名密碼,點擊Connect便可進入控制檯界面,以下所示:
在空白處能夠輸入符合SQL規範的語句對數據庫進行操做,左側邊欄能夠看到有一個T_USER數據庫表,這是JPA幫咱們建立的,在h2中,表的名字默認都是大寫的,可是在寫SQL語句的時候可使用小寫,h2會幫咱們轉換成大寫形式。以下所示:
關於h2數據庫的介紹就先這樣,由於h2的接口也符合JDBC規範,因此若是熟悉JDBC的話,不須要太關注h2的操做細節。
TDD即Test-Driven Development (測試驅動開發)。名字可能不那麼好理解其意義,什麼是測測試驅動開發?爲何要用測試來啓動開發?測試如何驅動開發的?下面將圍繞這三個問題簡單介紹一下TDD。
若是以前沒有接觸過相似的概念,大多數人對測試的認識應該是:先編寫代碼,完成以後再進行測試,測試的目的是檢驗程序的正確性、性能質量、可用性、可伸縮性等。而測試驅動開發則偏偏相反,TDD提倡的是先編寫測試程序,而後編寫代碼知足測試成功,使得測試程序能經過,只要測試用例寫的好,重構代碼的時候須要考慮的事情就能夠少不少,只須要讓代碼能經過測試便可。
TDD和傳統的先開發後測試的方式相比,至少有以下幾個好處:
其實上面隱隱有提到過這點,但沒有明確給出一個思路或者步驟,下面是TDD的基本流程:
這裏有一個問題,步驟2顯然是確定會失敗的(由於尚未編寫具體的代碼),爲何還要運行一次測試程序呢?由於失敗的緣由有不少,不必定就是由於尚未編寫具體代碼致使,也有多是測試環境有問題致使的,因此先運行一次,查看錯誤報告,若是是測試環境有問題,那麼就先嚐試修復測試環境,不然若是在有問題的測試環境下進行開發,可能會致使不管怎麼編寫程序都不可能經過測試的狀況(由於每次測試都會由於測試環境的問題致使測試失敗)。
實際上,TDD遠不止如此,還有不少不少好處,也有一些弊端,由於我本人對TDD的瞭解也不算多,平時由於比較懶,也沒有養成先寫測試的習慣,因此就很少說了,建議自行搜索相關資料進行學習,這裏就當是「拋磚引玉」吧。
本文介紹了單元測試的概念,順帶介紹了兩個測試框架JUnit,Mockito的簡單使用,隨後還結合Spring Boot項目作了一次小實踐,但願對讀者有幫助。最後還簡單介紹了TDD(測試驅動開發),TDD是敏捷開發中的一項核心技術,能夠有效的提升開發效率和產品質量,TDD其實也算是一門學問,若是想要深刻學習,推薦到這裏看看。