文章目錄
所有知識體系文章,GitHub已收錄,歡迎Star!
GitHub地址: https://github.com/Ziphtracks/JavaLearningmanual
搜索關注微信公衆號“碼出Offer”,Z哥送你學習福利資源!
Junit單元測試
一、什麼是單元測試?
在計算機編程中,單元測試(英語:Unit Testing)又稱爲模塊測試, 是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。 程序單元是應用的最小可測試部件。簡單來說,就是測試數據的穩定性是否達到程序的預期。
二、單元測試的重要性
談到測試,我們爲什麼要對程序進行測試呢?測試會爲程序帶來什麼好處呢?
首先,我們每個人都會犯錯誤。畢竟人嘛,沒有完美的誰誰誰。在程序中犯錯誤就像生活中犯錯一樣,錯誤不是一天兩天而形成的。當需要改的時候,也不是能花少的時間而改掉的。這裏我談到的程序中的錯誤,就是著名的Bug。
我們可能在不經意間寫錯,如果你到了最後階段去檢驗項目成果時,發現會有錯誤,這時候我們很難找到Bug的源頭在哪裏。我們都知道,有可能一處出錯會導致步步錯的情況。
然而,測試就在我們的上述說法中,顯得尤爲重要。有了測試的概念,這時候當我們做完項目的一個小模塊,我們先去測試一下這個小模塊是否正確或達到預期,如果錯誤或者沒有達到預期就需要反覆修改,直到正確或達到預期。這裏所說的也就是使用了單元測試。
當我們一塊一塊的做完並一塊一塊的測試後OK後,這時候你會發現項目像拼圖一樣拼在了一起。簡單來說,這就是單元測試存在的重要意義!
聲明: 術語顯得過於生硬,白話文也許會讓你們瞭解,請諒解我的大白話!謝謝!
三、黑盒測試與白盒測試
3.1 黑盒測試
黑盒測試又稱功能測試。它通過測試來檢驗程序是否能正常使用。在測試過程中,我們把程序看作爲一個打不開的盒子,黑黑的什麼也看不見,內部代碼怎麼寫的也不知道。也就是說完全不考慮任何內部結構和性能的情況下爲程序傳入(Input)參數,然後去查看程序輸出(Output)是否在正常範圍內,通常這時候我們需要多此測試才得出結論。
特點: 不需要我們中間參與編寫任何代碼,傳入參數值後查看程序是否正常或達到預期值。
3.2 白盒測試
白盒測試又稱結構測試。在這裏白盒測試與黑盒測試不同,在測試過程中,我們可以把程序看作爲一個可以看到的白色透明盒子,我們清楚盒子內部的代碼和結構。我們在使用白盒測試的時候,測試人員必須檢查程序的內部結構,而要從檢查程序的邏輯開始,一步一步的檢查傳入參數(Input)並查看程序的運行過程和輸出(Output)結果,最終得出測試數據。這也就是“白盒測試”爲什麼叫窮舉路徑測試的原因,再次強調,是因爲我們清楚程序的內部結構和代碼,從而檢查所有結構的正確與否和預期值。
注意: 單元測試就是白盒測試的一種!
四、單元測試思想傳遞
在這裏我們忘掉單元測試,使用平時我們自己測試的方式來測試數據,看看它有什麼缺點。
首先,我先創建在一個計算器類,在其中隨便創建兩個運算方法,供我們模擬測試。
package com.mylifes1110.java;
/**
* 計算器
*/
public class Calculator {
/**
* 加法
*/
public int add(int num1, int num2) {
return num1 + num2;
}
/**
* 減法
*/
public int cut(int num1, int num2) {
return num1 - num2;
}
}
然後我們再去編寫測試類,創建對象,先去測試加法。
package com.mylifes1110.java;
public class Test {
public static void main(String[] args) {
Calculator calculator = new Calculator();
//測試加法
System.out.println(calculator.add(10, 10)); //20 正確
}
}
測試後,我們查看結果爲正確的,然後進行下一步測試。因爲我們有兩條數據需要測試,平時在測試完一條數據後需要把測試過的數據註釋掉,再進行接下來的測試。如下:
package com.mylifes1110.java;
//測試類
public class Test {
public static void main(String[] args) {
Calculator calculator = new Calculator();
//測試加法
// System.out.println(calculator.add(10, 10)); //20 正確
//測試減法
System.out.println(calculator.cut(10, 10)); //0 正確
}
}
測試完兩條數據後,再去繼續編寫我們的項目代碼。
其實,我們有沒有發現這樣做很麻煩呢?上一步驟爲什麼需要把測試過的數據註釋掉呢?
答案來了,的確很麻煩,至於爲什麼註釋掉,那是因爲我們在寫項目代碼的時候,需要測試,不可能在同一個測試類測試這麼多數據。而且在測試的過程序,數據與數據之間是有關聯是互相影響的。這就會造成我們的測試不準確從而影響後續編碼進度和項目準確性。
瞭解了上述測試的缺點,我們也需要了解單元測試的思想了。單元測試需要擁有什麼樣的特點才能解決掉上述測試的麻煩呢?其實我們的單元測試也是通過編碼規範來約束的。至於編碼規範嘛,你還不去看第五章?
五、單元測試的編碼規範
單元測試的編碼規範有這幾條,小夥伴們拿小本本記好了!
- 類名: 定義測試類,類名是由
被測試類名Test
構成。例如:CalculatorTest- 包名: 定義的測試類需要放在
xxx.xxx.xxx.test
包中。例如:package com.mylifes1110.test;- 方法名: 測試方法的方法名有兩種定義方式
test測試方法
和測試方法
。例如:testAdd和add- 返回值: 因爲我們的方法只是在類中測試,可以獨立運行,所以不需要處理任何返回值,所以這裏使用
void
。例如:public void add();- 參數列表: 因爲我們的方法是用來測試的,至於參數列表的傳入是沒有必要的。我們在測試的時候自行傳入需要的參數測試即可。所以在此參數列表爲
空
。例如:例如:public void add();- @Test註解: 測試是需要運行來完成的。如果我們只有一個main方法,顯然在結構上還是需要我們去註釋掉測試過的。解決此問題這裏我們需要在測試方法上方加
@Test
註解來完成測試,只要是加該註解的方法,可以單獨運行此方法來完成測試。- @Test註解jar包Junit4、5: @Test註解是需要我們導入jar包才能使用的。jar包有兩個分別是:
junit-4.13-rc-2
和hamcrest-core-1.3
。這裏我使用的是Junit4,單元測試還有Junit5,版本差異我沒有做了解。主要是可以完成測試纔是硬道理!- IDEA快捷導入Junit4、5: 使用IDEA的小夥伴,你們的福音來了。我們可以先創建測試類和方法,然後在測試方法上方加入
@Test
註解,此時IDEA顯示的@Test註解是飄紅的,這時候我們使用Alt + Enter
組合鍵來打開導入Junit單元測試列表,然後再選擇Junit4或者Junit5確定即可導入成功!這時候再查看註解就沒有飄紅了!
六、@Test測試與Assert斷言步驟
斷言方法 | 描述 |
---|---|
assertNull(java.lang.Object object) | 檢查對象是否爲空 |
assertNotNull(java.lang.Object object) | 檢查對象是否不爲空 |
assertEquals(long expected, long actual) | 檢查long類型的值是否相等 |
assertEquals(double expected, double actual, double delta) | 檢查指定精度的double值是否相等 |
assertFalse(boolean condition) | 檢查條件是否爲假 |
assertTrue(boolean condition) | 檢查條件是否爲真 |
assertSame(java.lang.Object expected, java.lang.Object actual) | 檢查兩個對象引用是否引用同一對象(即對象是否相等) |
assertNotSame(java.lang.Object unexpected, java.lang.Object actual) | 檢查兩個對象引用是否不引用統一對象(即對象不等) |
首先,我們先去按照Junit單元測試規範來書寫測試代碼,如下:
然後我們會發現每一個需要測試的方法左邊都有一個綠色的小三角,這是用來單元測試運行的。也就是說,我們可以只運行某一個方法去測試。現在我們去運行add()方法,結果如下:
這時候,我們發現控制檯是綠色的並輸出的打印結果,這說明我們的程序沒有問題。如果我再其中加入一個算數異常會有怎麼樣的結果呢?如下:
在這裏我們會發現,控制檯變爲了紅色,並給出來報錯信息。這證明了我們的程序測試後出現了問題。這僅是程序正確與失敗的關係。
如果我們需要一個預期值呢?那麼測試的結果不是我想要的預期值,而程序還是綠色的,證明程序沒有問題怎麼辦呢?有的小夥伴會說,我們已經查看了打印控制檯的信息,打印結果不是預期值就說明程序有問題,需要去修改唄。對,其實這樣說是沒有任何毛病的。但是,我們在開發中,如果由於你的疏忽或者疲勞看到了綠色就覺得程序沒有問題怎麼辦呢?所以面對這個問題,我們在單元測試的時候,儘量不要去打印預期值,需要注重觀察是綠色和紅色比較好,它可以直觀的反映程序的是否準確性和達到預期值。
這時候,我們就需要引入一個對象的靜態方法來斷言是否爲預期值。
Assert.assertEquals(預期值, 結果);
這時候我們發現Assert句點出來的方法可以既可以斷言數組,也可以斷言普通數據類型。所以這時候我們就來使用它斷言預期值。如下:
這時候,我們就斷言result結果的預期值爲10。斷言後,發現未達到預期值就會報錯!
注意: 我們使用斷言的時候儘量不要去斷言Double對象。對於雙精度數,絕對有必要使用增量進行比較,以避免浮點舍入的問題。如果您使用assertEquals
帶有double
參數的3參數版本。
assertEquals(double expected, double actual, double delta);
這樣以來Double
將被自動取消裝箱,double
並且一切正常,這樣測試結果就不會失敗。否則使用兩個參數的來斷言double類型,會有如下報錯信息:
七、@Before和@After註解
我們在上述,你是否會發現有一些重複操作呢?比如,我們每一個方法都需要去new對象。有些聰明的小夥伴會說,我們可以把它提到類的裏面與方法同級。對,這個處理方式也是一個正解。
但是我們在Junit單元測試中,有一個@Before註解,是用作資源的申請。也就是被@Before註解修飾的的方法會在測試方法之前自動執行。所以,我們可以去定義一個init方法,去初始化這個創建對象的過程。這就是@Before註解的作用!
有些應用場景,比如IO流的讀寫操作。如果我們要測試此代碼,是需要一個關閉流的過程,通過我們關閉流使用finally塊來保證最後流的關閉操作。這時,我們在Junit單元測試中,有一個@After註解,是用作資源的關閉。也就是說被@After註解修飾的方法會在測試方法之後自定執行。所以,我們在特定需要保證最後關閉、銷燬資源的時候,可以去定義一個close方法,去關閉或銷燬資源。這就是@After註解的作用!
注意: @Before和@After註解在程序報錯的時候,仍然可以保證數據的初始化和關閉銷燬,兩個方法是依舊執行的。這裏有點像我們tomact服務器的初始階段和銷燬階段,它們的執行不受任何影響。
八、自定義@MyTest註解實現單元測試
目的: 完成自定義註解@MyTest,並實現標有註解的方法並啓動它。(模擬@Test註解做單元測試)
步驟:
- 新建一個註解類(annotation),命名爲MyTest
- 創建一個TestJunit單元測試類,寫幾個方法,比如:
public void test1()
- 創建一個MyTestDemo測試類(主功能實現類),該類主要利用反射機制來實現對TestJunit單元測試類中加@MyTest註解方法的啓動
- 給予註解類生命週期與反射機制吻合,也就是定義的註解可以保留到運行時,通過反射機制可以獲取註解信息
- 編寫MyTestDemo測試類,利用反射獲取TestJunit單元測試類的Class對象,並獲取單元測試類中所有的方法對象,遍歷所有方法對象,只要加@MyTest的註解的方法把他執行起來,不加註解的不給予任何處理操作
- 啓動測試類,查看結果(執行結果,在最後面!)
注意:
- 自定義註解類中,沒有編寫註解體,也就是沒有給默認value值。因爲該註解只是起到了標識的作用,標識需要啓動的方法
- 註解類編譯後也是.class文件
- 通過反射機制來完成自定義註解操作,一定要給與註解和反射同樣的生命週期
- 你要知道我們是不能完成Junit4、Junit5這樣類型的插件功能的,可以選擇性的執行加了註解的方法,而且我們有實力寫出插件IDEA也是不承認的。不會給你生成run方法啓動項
MyTest註解類
package com.mylifes1110.java.anno;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* 此自定義註解@MyTest只是作爲需要單元測試的標記,不需要做默認值
* @Retention註解表示給與@MyTest註解生命週期
* 當前定義的註解可以保留到運行時,通過反射機制可以獲取註解信息
* 否則反射將對註解沒有任何作用,失去了該意義和自定義單元測試的初衷
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
// String value() default "";
}
TestJunit單元測試類
package com.mylifes1110.java.test.junit;
import com.mylifes1110.java.anno.MyTest;
import org.junit.Test;
public class TestJunit {
@Test
public void test1() {
System.out.println("---test1---");
}
@MyTest
public void test2() {
System.out.println("---test2---");
}
@MyTest
public void test3() {
System.out.println("---test3---");
}
public void test4() {
System.out.println("---test4---");
}
}
MyTestDemo測試類(主要實現功能並測試)
package com.mylifes1110.java.test.junit;
import com.mylifes1110.java.anno.MyTest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 讓自定義的MyTest註解起作用
* 通過反射,掃描TestJunit類中有哪些方法上方加了MyTest註解
* 如果加@MyTest註解的則執行它
* 如果沒有加@MyTest註解的則不做任何處理
*/
public class MyTestDemo {
public static void main(String[] args) {
/**
* 1.獲取TestJunit類對應的Class對象
*/
Class<TestJunit> junitClass = TestJunit.class;
/**
* 獲取TestJunit類中所有的方法對象
*/
Method[] methods = junitClass.getMethods();
/**
* 遍歷所有方法對象查找有與沒有@MyTest註解,並做出響應處理
*/
for (Method method : methods) {
boolean present = method.isAnnotationPresent(MyTest.class);
/**
* TestJunit類中有@MyTest註解的執行該方法
*/
if (present) {
try {
method.invoke(junitClass.newInstance());
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
e.printStackTrace();
}
} else {
/**
* TestJunit類中沒有@MyTest註解的不做任何操作
* 此else分支冗餘,只是爲了做標記,讓你們好理解
*/
}
}
}
}
執行結果圖