測試是開發的一個很是重要的方面,能夠在很大程度上決定一個應用程序的命運。良好的測試能夠在早期捕獲致使應用程序崩潰的問題,但較差的測試每每老是致使故障和停機。java
雖然有三種主要類型的軟件測試:單元測試,功能測試和集成測試,可是在這篇博文中,咱們將討論開發人員級單元測試。在我深刻講述具體細節以前,讓咱們先來回顧一下這三種測試的詳細內容。數據庫
單元測試用於測試各個代碼組件,並確保代碼按照預期的方式工做。單元測試由開發人員編寫和執行。大多數狀況下,使用JUnit或TestNG之類的測試框架。測試用例一般是在方法級別寫入並經過自動化執行。服務器
集成測試檢查系統是否做爲一個總體而工做。集成測試也由開發人員完成,但不是測試單個組件,而是旨在跨組件測試。系統由許多單獨的組件組成,如代碼,數據庫,Web服務器等。集成測試可以發現如組件佈線,網絡訪問,數據庫問題等問題。網絡
功能測試經過將給定輸入的結果與規範進行比較來檢查每一個功能是否正確實現。一般,這不是在開發人員級別的。功能測試由單獨的測試團隊執行。測試用例基於規範編寫,而且實際結果與預期結果進行比較。有若干工具可用於自動化的功能測試,如Selenium和QTP。app
如前所述,單元測試可幫助開發人員肯定代碼是否正常工做。在這篇博文中,我將提供在Java中單元測試的有用提示。框架
Java提供了若干用於單元測試的框架。TestNG和JUnit是最流行的測試框架。JUnit和TestNG的一些重要功能:dom
EasyMock是一個模擬框架,是單元測試框架,如JUnit和TestNG的補充。EasyMock自己不是一個完整的框架。它只是添加了建立模擬對象以便於測試的能力。例如,咱們想要測試的一個方法能夠調用從數據庫獲取數據的DAO類。在這種狀況下,EasyMock可用於建立返回硬編碼數據的MockDAO。這使咱們可以輕鬆地測試咱們意向的方法,而沒必要擔憂數據庫訪問。ide
測試驅動開發(TDD)是一個軟件開發過程,在這過程當中,在開始任何編碼以前,咱們基於需求來編寫測試。因爲尚未編碼,測試最初會失敗。而後寫入最小量的代碼以經過測試。而後重構代碼,直到被優化。模塊化
目標是編寫覆蓋全部需求的測試,而不是一開始就寫代碼,卻可能甚至都不能知足需求。TDD是偉大的,由於它致使簡單的模塊化代碼,且易於維護。整體開發速度加快,容易發現缺陷。此外,單元測試被建立做爲TDD方法的副產品。函數
然而,TDD可能不適合全部的狀況。在設計複雜的項目中,專一於最簡單的設計以便於經過測試用例,而不提早思考可能會致使巨大的代碼更改。此外,TDD方法難以用於與遺留系統,GUI應用程序或與數據庫一塊兒工做的應用程序交互的系統。另外,測試須要隨着代碼的改變而更新。
所以,在決定採用TDD方法以前,應考慮上述因素,並應根據項目的性質採起措施。
代碼覆蓋率衡量(以百分比表示)了在運行單元測試時執行的代碼量。一般,高覆蓋率的代碼包含未檢測到的錯誤的概率要低,由於其更多的源代碼在測試過程當中被執行。測量代碼覆蓋率的一些最佳作法包括:
高代碼覆蓋不能保證測試是完美的,因此要當心!
下面的 concat 方法接受布爾值做爲輸入,而且僅當布爾值爲true時附加傳遞兩個字符串:
public String concat(boolean append, String a,String b) {
String result = null;
If (append) {
result = a + b;
}
return result.toLowerCase();
}
如下是上述方法的測試用例:
@Test
public void testStringUtil() {
String result = stringUtil.concat(true, "Hello ", "World");
System.out.println("Result is "+result);
}
在這種狀況下,執行測試的值爲true。當測試執行時,它將經過。當代碼覆蓋率工具運行時,它將顯示100%的代碼覆蓋率,由於 concat 方法中的全部代碼都被執行。可是,若是測試執行的值爲false,則將拋出 NullPointerException 。因此100%的代碼覆蓋率並不真正代表測試覆蓋了全部場景,也不能說明測試良好。
在JUnit4以前,測試用例要運行的數據必須硬編碼到測試用例中。這致使了限制,爲了使用不一樣的數據運行測試,測試用例代碼必須修改。可是,JUnit4以及TestNG支持外部化測試數據,以即可以針對不一樣的數據集運行測試用例,而無需更改源代碼。
下面的 MathChecker 類有方法能夠檢查一個數字是不是奇數:
public class MathChecker {
public Boolean isOdd(int n) {
if (n%2 != 0) {
return true;
} else {
return false;
}
}
}
如下是MathChecker類的TestNG測試用例:
public class MathCheckerTest {
private MathChecker checker;
@BeforeMethod
public void beforeMethod() {
checker = new MathChecker();
}
@Test
@Parameters("num")
public void isOdd(int num) {
System.out.println("Running test for "+num);
Boolean result = checker.isOdd(num);
Assert.assertEquals(result, new Boolean(true));
}
}
如下是testng.xml(用於TestNG的配置文件),它具備要爲其執行測試的數據:
<?xml version="1.0" encoding="UTF-8"?>
<suite name="ParameterExampleSuite" parallel="false">
<test name="MathCheckerTest">
<classes>
<parameter name="num" value="3"></parameter>
<class name="com.stormpath.demo.MathCheckerTest"/>
</classes>
</test>
<test name="MathCheckerTest1">
<classes>
<parameter name="num" value="7"></parameter>
<class name="com.stormpath.demo.MathCheckerTest"/>
</classes>
</test>
</suite>
能夠看出,在這種狀況下,測試將執行兩次,值3和7各一次。除了經過XML配置文件指定測試數據以外,還能夠經過DataProvider註釋在類中提供測試數據。
與TestNG相似,測試數據也能夠外部化用於JUnit。如下是與上述相同MathChecker類的JUnit測試用例:
@RunWith(Parameterized.class)
public class MathCheckerTest {
private int inputNumber;
private Boolean expected;
private MathChecker mathChecker;
@Before
public void setup(){
mathChecker = new MathChecker();
}
// Inject via constructor
public MathCheckerTest(int inputNumber, Boolean expected) {
this.inputNumber = inputNumber;
this.expected = expected;
}
@Parameterized.Parameters
public static Collection<Object[]> getTestData() {
return Arrays.asList(new Object[][]{
{1, true},
{2, false},
{3, true},
{4, false},
{5, true}
});
}
@Test
public void testisOdd() {
System.out.println("Running test for:"+inputNumber);
assertEquals(mathChecker.isOdd(inputNumber), expected);
}
}
能夠看出,要對其執行測試的測試數據由getTestData()方法指定。此方法能夠輕鬆地修改成從外部文件讀取數據,而不是硬編碼數據。
許多新手開發人員習慣於在每行代碼以後編寫System.out.println語句來驗證代碼是否正確執行。這種作法經常擴展到單元測試,從而致使測試代碼變得雜亂。除了混亂,這須要開發人員手動干預去驗證控制檯上打印的輸出,以檢查測試是否成功運行。更好的方法是使用自動指示測試結果的斷言。
下面的 StringUti 類是一個簡單類,有一個鏈接兩個輸入字符串並返回結果的方法:
public class StringUtil {
public String concat(String a,String b) {
return a + b;
}
}
如下是上述方法的兩個單元測試:
@Test
public void testStringUtil_Bad() {
String result = stringUtil.concat("Hello ", "World");
System.out.println("Result is "+result);
}
@Test
public void testStringUtil_Good() {
String result = stringUtil.concat("Hello ", "World");
assertEquals("Hello World", result);
}
testStringUtil\_Bad將始終傳遞,由於它沒有斷言。開發人員須要手動地在控制檯驗證測試的輸出。若是方法返回錯誤的結果而且不須要開發人員干預,則testStringUtil\_Good將失敗。
一些方法不具備肯定性結果,即該方法的輸出不是預先知道的,而且每一次均可以改變。例如,考慮如下代碼,它有一個複雜的函數和一個計算執行復雜函數所需時間(以毫秒爲單位)的方法:
public class DemoLogic {
private void veryComplexFunction(){
//This is a complex function that has a lot of database access and is time consuming
//To demo this method, I am going to add a Thread.sleep for a random number of milliseconds
try {
int time = (int) (Math.random()*100);
Thread.sleep(time);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public long calculateTime(){
long time = 0;
long before = System.currentTimeMillis();
veryComplexFunction();
long after = System.currentTimeMillis();
time = after - before;
return time;
}
}
在這種狀況下,每次執行 calculateTime 方法時,它將返回一個不一樣的值。爲該方法編寫測試用例不會有任何用處,由於該方法的輸出是可變的。所以,測試方法將不能驗證任何特定執行的輸出。
一般,開發人員會花費大量的時間和精力編寫測試用例,以確保應用程序按預期工做。然而,測試負面測試用例也很重要。負面測試用例指的是測試系統是否能夠處理無效數據的測試用例。例如,考慮一個簡單的函數,它能讀取長度爲8的字母數字值,由用戶鍵入。除了字母數字值,應測試如下負面測試用例:
相似地,邊界測試用例測試系統是否適用於極端值。例如,若是用戶但願輸入從1到100的數字值,則1和100是邊界值,對這些值進行測試系統是很是重要的。