一、實驗目的
1)掌握單元測試的方法;
- 學習XUnit測試原理及框架;
3)掌握使用測試框架進行單元測試的方法和過程;
二、實驗內容與要求
1.了解單元測試
1.1單元測試的原理
單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證。對於單元測試中單元的含義,一般來說,要根據實際情況去判定其具體含義,如C語言中單元指一個函數,Java里單元指一個類,圖形化的軟件中可以指一個窗口或一個菜單等。總的來說,單元就是人為規定的最小的被測功能模塊。單元測試是在軟件開發過程中要進行的最低級別的測試活動,軟件的獨立單元將在與程序的其他部分相隔離的情況下進行測試。單元測試是由程序員自己來完成,最終受益的也是程序員自己。可以這么說,程序員有責任編寫功能代碼,同時也就有責任為自己的代碼編寫單元測試。執行單元測試,就是為了證明這段代碼的行為和我們期望的一致。
單元測試的內容包括
- 模塊接口測試
- 局部數據結構測試
- 路徑測試
- 錯誤處理測試
- 邊界測試
(1)模塊接口測試
模塊接口測試是單元測試的基礎。只有在數據能正確流入、流出模塊的前提下,其他測試才有意義。模塊接口測試也是集成測試的重點,這里進行的測試主要是為后面打好基礎。測試接口正確與否應該考慮下列因素
- 輸入的實際參數與形式參數的個數是否相同
- 輸入的實際參數與形式參數的屬性是否匹配
- 輸入的實際參數與形式參數的量綱是否一致
- 調用其他模塊時所給實際參數的個數是否與被調模塊的形參個數相同;
- 調用其他模塊時所給實際參數的屬性是否與被調模塊的形參屬性匹配;
- 調用其他模塊時所給實際參數的量綱是否與被調模塊的形參量綱一致;
- 調用預定義函數時所用參數的個數、屬性和次序是否正確;
- 是否存在與當前入口點無關的參數引用;
- 是否修改了只讀型參數;
- 對全程變量的定義各模塊是否一致;
- 是否把某些約束作為參數傳遞。
如果模塊功能包括外部輸入輸出,還應該考慮下列因素:
-
文件屬性是否正確;
-
OPEN/CLOSE語句是否正確;
-
格式說明與輸入輸出語句是否匹配;
-
緩沖區大小與記錄長度是否匹配;
-
文件使用前是否已經打開;
-
是否處理了文件尾;
-
是否處理了輸入/輸出錯誤;
-
輸出信息中是否有文字性錯誤。
-
局部數據結構測試;
-
邊界條件測試;
-
模塊中所有獨立執行通路測試;
(2)局部數據結構測試
檢查局部數據結構是為了保證臨時存儲在模塊內的數據在程序執行過程中完整、正確,局部功能是整個功能運行的基礎。重點是一些函數是否正確執行,內部是否運行正確。局部數據結構往往是錯誤的根源,應仔細設計測試用例,力求發現下面幾類錯誤:
-
不合適或不相容的類型說明;
-
變量無初值;
-
變量初始化或省缺值有錯;
-
不正確的變量名(拼錯或不正確地截斷);
-
出現上溢、下溢和地址異常。
(3)邊界條件測試
邊界條件測試是單元測試中最重要的一項任務。眾所周知,軟件經常在邊界上失效,采用邊界值分析技術,針對邊界值及其左、右設計測試用例,很有可能發現新的錯誤。邊界條件測試是一項基礎測試,也是后面系統測試中的功能測試的重點,邊界測試執行的較好,可以大大提高程序健壯性。
(4)獨立路徑測試
在模塊中應對每一條獨立執行路徑進行測試,單元測試的基本任務是保證模塊中每條語句至少執行一次。測試目的主要是為了發現因錯誤計算、不正確的比較和不適當的控制流造成的錯誤。具體做法就是程序員逐條調試語句。常見的錯誤包括:
-
誤解或用錯了算符優先級;
-
混合類型運算;
-
變量初值錯;
-
精度不夠;
-
表達式符號錯。
(5)錯誤處理測試
檢查模塊的錯誤處理功能是否包含有錯誤或缺陷。例如,是否拒絕不合理的輸入;出錯的描述是否難以理解、是否對錯誤定位有誤、是否出錯原因報告有誤、是否對錯誤條件的處理不正確;在對錯誤處理之前錯誤條件是否已經引起系統的干預等。
通常單元測試在編碼階段進行。在源程序代碼編制完成,經過評審和驗證,確認沒有語法錯誤之后,就開始進行單元測試的測試用例設計。利用設計文檔,設計可以驗證程序功能、找出程序錯誤的多個測試用例。對於每一組輸入,應有預期的正確結果。
1.2 測試框架
xUnit是各種代碼驅動測試框架的統稱,這些框架可以測試 軟件的不同內容(單元),比如函數和類。xUnit框架的主要優點是,它提供了一個自動化測試的解決方案。可以避免多次編寫重復的測試代碼。底層是xUnit的framwork,xUnit的類庫,提供了對外的功能方法、工具類、api等
TestCase(具體的測試用例)去使用framwork
TestCase執行后會有TestResult
使用TestSuite控制TestCase的組合
TestRunner執行器,負責執行case
TestListener過程監聽,監聽case成功失敗以及數據結果,輸出到結果報告中
1.3面向特定語言的,基於xUnit框架的自動化測試框架
Junit : 主要測試用Java語言編寫的代碼
CPPunit:主要測試用C++語言編寫的代碼
unittest , PyUnit:主要測試用python語言編寫的代碼
MiniUnit: 主要用於測試C語言編寫的代碼
三、實驗過程
由於代碼運行在Java環境,所以采用Junit來完成測試。
3.1 源碼
Elward-lv的Git源碼,未修改的commitId:907a01ac73a4382edca6194dcc4f63045d11c918,代碼如下:
package test.lifeGame;
import java.awt.Graphics;
import javax.swing.JPanel;
import java.util.Random;
/**
* Active 表示活,Dead 表示死
*/
public class gameDemo extends JPanel implements Runnable {
static enum CellStatus {
Active ,
dead
}
private boolean isCancel = false; //自定義一個終止標志位
private CellStatus [][] generation1 ;
private CellStatus [][] generation2 ;
private int MaxRow = 50 ;
private int MaxCol = 50 ;
private int rows;
private int cols;
private float frequenceRate = 1 ;
@Override
public void run() {
System.out.println("線程開始") ;
while (true) {
synchronized (this) {
while ( isCancel ) {
try {
this.wait() ;
} catch (InterruptedException e) {
e.printStackTrace() ;
}
}
repaint() ;
try {
sleep() ;
} catch (InterruptedException e) {
e.printStackTrace() ;
}
changeGeneration() ;
}
}
}
public gameDemo(int rows , int cols) {
generationRandom(rows , cols ) ;
}
/**
* 根據給出得行列創建世界,初始化隨機生成矩陣
* @param rows
* @param cols
*/
public void generationRandom(int rows , int cols ) {
synchronized(this) {
if ( ! (rows <= MaxRow && cols <= MaxCol) ) {
System.out.println("cols,rows error") ;
return ;
}
this.rows = rows ;
this.cols = cols ;
generation1 = new CellStatus[cols][rows] ;
generation2 = new CellStatus[cols][rows] ;
for ( int i = 0 ; i < this.cols ; i++)
for (int j = 0 ; j < this.rows ; j++) {
Random r = new Random() ;
int z = r.nextInt(100);
if ( z > 50 ) {
generation1[i][j] = CellStatus.Active;
} else {
generation1[i][j] = CellStatus.dead;
}
}
this.notifyAll();
}
}
/**
* 根據規則改變矩陣
* @return
*/
public int changeGeneration() {
for(int i=0; i < rows; i++) {
for(int j=0; j <cols ; j++) {
judgeCellStatus(i,j);
}
}
CellStatus[][] temp =null;
temp = generation1;
generation1 = generation2;
generation2 = temp;
for(int i=0 ; i < rows ; i++) {
for(int j=0 ; j < cols ; j++) {
generation2[i][j] = CellStatus.dead;
}
}
return 1;
}
/**
* 判斷每個細胞周圍的活細胞個數 並且改變下一代的這個細胞的狀態
* @param col
* @param row
* @return
*/
int judgeCellStatus(int col , int row) {
int activeCount = 0 ;
if( (col-1) >= 0 && (row-1) >= 0 && (generation1[col-1][row-1] == CellStatus.Active) )
activeCount++;//
if( (col-1) >= 0 && (generation1[col-1][row] == CellStatus.Active))
activeCount++;
if( (col-1) >= 0 && (row+1) < rows && (generation1[col-1][row+1] == CellStatus.Active))
activeCount++;
if( (row-1) >= 0 && (generation1[col][row-1] == CellStatus.Active))
activeCount++;//
if( (row+1) < rows && (generation1[col][row+1] == CellStatus.Active ))
activeCount++;
if( (col+1) < cols && (row-1) >= 0 && (generation1[col+1][row-1] == CellStatus.Active))
activeCount++;
if( (col+1) < cols && (generation1[col+1][row] == CellStatus.Active))
activeCount++;
if( (col+1) < cols && (row+1) < rows && (generation1[col+1][row+1] == CellStatus.Active))
activeCount++;
if(activeCount == 3) {
generation2[col][row] = CellStatus.Active;
} else if(activeCount == 2) {
generation2[col][row] = generation1[col][row];
} else {
generation2[col][row] = CellStatus.dead;
}
return activeCount;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g) ;
for (int i=0 ; i < rows; i++ ) {
for (int j = 0; j < cols; j++) {
if (generation1[i][j] == CellStatus.Active) {
g.fillRect(j * 10, i * 10, 10, 10);
} else {
g.drawRect(j * 10, i * 10, 10, 10);
}
}
}
}
/**
* 終止開啟線程刷新
*/
public void cancle() {
isCancel = true;
}
public void Start() {
isCancel = false;
}
public void sleep() throws InterruptedException {
int rate = (int) frequenceRate * 1000;
Thread.sleep(rate);
}
public float getFrequenceRate() {
return frequenceRate;
}
/**
* 修改刷新得頻率,默認1s
* @param frequenceRate
*/
public void setFrequenceRate(float frequenceRate) {
if(frequenceRate > 0 && frequenceRate < 10){
this.frequenceRate = frequenceRate;
} else {
return ;
}
}
public CellStatus[][] getGeneration1() {
return generation1;
}
public CellStatus[][] getGeneration2() {
return generation2;
}
}
3.2 測試用例設計
程序主要包含兩個類,一個是用於生成GUI的GUI類,和一個用於描述生命游戲世界的gameDemo類。針對於GUI類,由於設計GUI的時候沒有進行很好的模塊划分,所以只能采用手動的方式進行單元測試,即手動進行數據的輸入和事件的觸發;對於gameDemo類的隨機生成二維數組,根據規則改變矩陣,判斷某個坐標細胞下一次的狀態,規則刷新線程的運行狀態等功能來單獨設計測試用例,因為細胞圖生成隨機所以很難斷言具體的參數。
a.隨機生成二維數組測試用例 generationRandom(int rows , int cols):輸入是矩陣的行列,范圍均是0<=x<=50的整數
用例描述 | 用例參數 | 期望結果 |
---|---|---|
正常用例 | rows = 20,cols = 30 | 返回20*30的隨機字符矩陣 |
邊界用例 | rows = 50,cols = 50 | 返回50*50的隨機字符矩陣 |
錯誤用例1 | rows = 51,cols = 51 | 矩陣大小不變,超出范圍被處理 |
錯誤用例2 | rows = -1,cols = -1 | 拋出異常NegativeArraySizeException |
b.判斷某個坐標細胞下一次的狀態用例 judgeCellStatus(int col , int row): 前提是數組的矩陣已經隨機生成,輸入是矩陣的行列號
用例描述 | 用例參數 | 期望結果 |
---|---|---|
正常用例 | row=10,col = 10 | 此細胞周圍的活細胞數目 |
邊界用例 | row = 0, col = 0 | 此細胞周圍活細胞數目 |
異常用例 | row ,col均大於設定的數組行列 | 數組越界異常 |
c.根據規則改變矩陣用例 changeGeneration():前提是數組的矩陣已經隨機生成,根據細胞生存規則改變細胞下一次狀態,因為沒有輸入只進行一般測試
用例描述 | 用例參數 | 期望結果 |
---|---|---|
一般用例 | 無 | 改變之后的矩陣 |
d.**規則刷新線程的運行狀態用例 **:前提是gameDemo已經初始化,因為沒有輸入,所以測試只進行一般的測試
用例描述 | 用例參數 | 期望結果 |
---|---|---|
一般用例 | 無 | 隨着時間的推移,細胞狀態圖不停刷新 |
e.針對GUI的測試用例: 主要針對GUI中事件和輸入文本,進行手動測試,輸入是行列號(和a的輸入一樣)和變化頻率,時間包括一個暫停按鈕。點擊按鈕時會收集輸入文本,並依照此來設置新的矩陣參數,所以主要測試GUI的流程的正確性和輸入文本的類型。
用例描述 | 用例參數 | 期望結果 |
---|---|---|
正常用例 | rows = "30",cols = "30",rate = "1.0" | 行列改變,按照規則刷新 |
異常用例 | rows = "30cols", cols = "30cols",rate = "0.5" | 提示異常的輸出信息 |
3.3 選擇的測試框架介紹、安裝過程
1)JUnit是由 Erich Gamma 和 Kent Beck 編寫的一個回歸測試框架(regression testing framework)。Junit測試是程序員測試,即所謂白盒測試,因為程序員知道被測試的軟件如何(How)完成功能和完成什么樣(What)的功能。Junit是一套框架,繼承TestCase類,就可以用Junit進行自動測試了。
2)安裝過程
idea安裝Junit:在idea的plugin里面搜索Junit
然后點擊安裝,隨后配置Junit如下:
使用方法可以直接鼠標右擊選擇goto:
創建新的測試,選擇需要測試的方法:
引入包:由於開始項目是由maven構架,所以只需要改變pom.xml就可以完成Junit包的引入,依賴如下圖:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
</dependencies>
3.4 測試代碼
a.隨機生成二維數組測試用例
package test.lifeGame;
import org.junit.Assert;
import org.junit.Test;
/**
* 測試隨機生成函數
*/
public class TestRandom {
private gameDemo game = new gameDemo(50,50);
@Test
public void generationRandom() {
game.generationRandom(20,30);
Assert.assertEquals(game.getGeneration1()[0].length,20);
Assert.assertEquals(game.getGeneration1().length,30);
}
@Test
public void generationRandom2(){
game.generationRandom(50,50);
Assert.assertEquals(game.getGeneration1()[0].length,50);
Assert.assertEquals(game.getGeneration1().length,50);
}
@Test(expected = NegativeArraySizeException.class)
public void generationRandom3(){
game.generationRandom(-1,-1);
}
@Test
public void generationRandomOut(){
game.generationRandom(51,51);
}
}
b,c,d.判斷某個坐標細胞下一次的狀態用例和根據規則改變矩陣用例和測試線程的用例
package test.lifeGame;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* 測試按照規則更新矩陣 以及判斷方格周圍格子狀態
*/
public class TestChangeAndJudge {
private gameDemo game = new gameDemo(10,10);
@Before
public void setUp() throws Exception {
System.out.println("測試開始");
}
@After
public void tearDown() throws Exception {
System.out.println("測試結束");
}
@Test
public void changeGeneration() {
gameDemo.CellStatus[][] statuses = game.getGeneration1();
showCellStatus(statuses);
game.changeGeneration();
showCellStatus(game.getGeneration1());
}
@Test
public void judgeCellStatus() {
showCellStatus(game.getGeneration1());
int count = game.judgeCellStatus(0,0);
System.out.println("count:"+count);
}
@Test(expected = ArrayIndexOutOfBoundsException.class)
public void judgeCellStatusOut() {
int count = game.judgeCellStatus(10,10);
System.out.println("count:"+count);
}
@Test
public void judgeCellStatusNormal() {
game.judgeCellStatus(5,5);
}
public void showCellStatus(gameDemo.CellStatus[][] statuses){
for(int i=0;i<10;i++){
for(int j=0;j<10;j++){
System.out.printf("%8s",statuses[i][j]);
}
System.out.println();
}
System.out.println("***********完成***************");
}
}
package test.lifeGame;
import org.junit.After;
import org.junit.Test;
import org.mockito.Mock;
import static org.junit.Assert.*;
public class TestThreadRun {
private gameDemo game = new gameDemo(10,10);
Thread thread;
@After
public void tearDown() throws Exception{
thread.stop();
}
/**
* 測試根據規則改變矩陣線程的函數
*/
@Test
public void run() {
thread = new Thread(game);
thread.start();
}
}
3.5 測試結果與分析
a.隨機生成二維數組測試用例
用例描述 | 用例參數 | 實際結果 |
---|---|---|
正常用例 | rows = 20,cols = 30 | 字符矩陣 20* 30 ,通過 |
邊界用例 | rows = 50,cols = 50 | 字符矩陣 50* 50 ,通過 |
錯誤用例1 | rows = 51,cols = 51 | 測試通過 |
錯誤用例2 | rows = -1,cols = -1 | 測試通過 |
b,c,d均測試通過,沒有問題
e.GUI測試用例
用例描述 | 用例參數 | 實際結果 |
---|---|---|
正常用例 | rows = "30",cols = "30",rate = "1.0" | 沒有問題,程序變化成30*30之后,繼續運行 |
錯誤用例 | rows = "30cols", cols = "30cols",rate = "0.5" | 出現異常,程序停止,如下圖 |
經過測試,在含有錯誤用例的結果可以看出,有一些程序需要處理的異常,分別是數組越界,參數不合法為負,以及GUI讀取字符串時含有字母等非法字符導致轉化為float時出現問題等。解決方法可以是在程序中捕獲這些異常並保持上次的狀態,然后提醒用戶輸入不合法或者其他信息。
3.6 提交檢查結果到倉庫
四.思考題和小結
小結
本次實驗主要是做了單元測試的工作,使用了一些單元測試的辦法來查找程序可能出現的bug,除了少數一些未知的異常除外,我感覺其他的異常在程序的設計階段都可以通過參數檢查和修改來達到控制輸入的目的。而且經過這次測試,我明白了模塊化設計的作用,不僅僅是方便維護檢查,也方便測試,更有利於程序的穩定!以后會努力往此方面靠,盡量做了模塊化設計。
思考題
比較以下二個工匠的做法,你認為哪種好?結合編碼和單元測試,談談你的認識。
答:其實問題和程序的先檢查還是最后檢查類似。對於編碼來說:編碼風格和約定自然是需要在開始的時候就需要確定,這樣的話通過注解和數據命名方便對代碼進一步的修改,而不需要花費多余的時間去理解可能遺忘了的功能的程序。對於測試來說,工匠一是在寫程序的時候就解決可能出現的bug,這樣的話自然會消耗更多的開發時間,而且不能保證解決所有的問題,比較適合時間充裕的開發工作;工匠二是先進行編碼不去考慮程序中數據超出預期可能出現一些異常,而是留到最后統一測試檢查問題,比較適合項目時間緊張的開發。兩種方式都有適合的場景,我覺得第二種更好一點,即使是課設時間也都是不是很充足的。