軟件開發及測試
項目地址
github項目地址:https://github.com/81BlingBling18/WordCount
本次項目中用到的PSP表格
PSP2.1 | PSP 階段 | 預估耗時 (分鐘) | 實際耗時 (分鐘) |
---|---|---|---|
Planning | 計劃 | 20 | 30 |
· Estimate | · 估計這個任務需要多少時間 | 15 | 14 |
Development | 開發 | 280 | 300 |
· Analysis | · 需求分析 (包括學習新技術) | 20 | 12 |
· Design Spec | · 生成設計文檔 | 15 | 18 |
· Design Review | · 設計復審 (和同事審核設計文檔) | 18 | 17 |
· Coding Standard | · 代碼規範 (為目前的開發制定合適的規範) | 30 | 24 |
· Design | · 具體設計 | 25 | 22 |
· Coding | · 具體編碼 | 150 | 300 |
· Code Review | · 代碼復審 | 20 | 20 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 45 | 50 |
Reporting | 報告 | 30 | 45 |
· Test Report | · 測試報告 | 20 | 15 |
· Size Measurement | · 計算工作量 | 23 | 20 |
· Postmortem & Process Improvement Plan | · 事後總結, 並提出過程改進計劃 | 15 | 10 |
合計 | 726 | 897 |
思路解析
這周軟件測試課程的任務是寫一個WordCount,要求可以看下面第一篇參考資料。
看過一遍要求之後就可以知道,這是一個從命令行獲得命令,將命令分解之後執行命令,匯總輸出結果,然後輸出結果的程序,這是程序的框架。程序的功能是統計字符數、單詞數等等,可以通過分解命令來實現。初步思路是程序應當包括以下三個部分:
- 命令處理:分析輸入的命令,例如是否要遞歸處理文件,是否要使用stopList等等,並生成內部控制數據結構實例。
- 命令執行控制:根據命令處理得到的結構體,分析如何執行命令,並且循環調用命令執行模塊,匯總執行結果,進行輸出等操作。
- 命令執行:執行簡單命令,產生運行結果並返回。
首先是命令處理部分,老師規定的實現語言是Java。main函數簽名是main(String[] args)
這裏的args就可以接收命令行中傳入的參數。然後分析命令特點[-c -w -l -s -a] [codeFileName] [-o] [outputFile] [-e] [stopListFile]
。也就是說-o
後面一定接輸出文件,-e
後面一定接stopListFile,剩下的就是codeFileName了。這樣就可以直接switch-case
處理。
剩下的就是分析是否指定-s
參數,是的話,遞歸掃描文件夾,找出所有符合條件的文件路徑,否則直接分析指定的文件。分析完成之後生成數據結構實例。
命令執行控制部分比較簡單,根據數據結構實例拆解命令,然後循環分析每個文件,匯總每次的執行結果,並且輸出到-o
指定的文件,或者默認的result.txt
就行了
命令執行這個接收命令,輸出執行結果。每一個命令如-s
設置一個flag,為true
時表示需要進行相應的分析,編寫相應的處理方法並調用即可。需要註意的是,在命令執行之前,若指定了stopList則需要進行相應的初始化。
程序設計實現
綜上所述,至少應當包括四個類
- WordCount:命令執行控制
- CommandDecoder:命令處理
- Processor:命令執行
- Commands:相當於命令數據結構體
下面給出整體的流程圖:
下面給出每個類中的方法簽名、返回值以及功能註釋:
public class WordCount {
public static void main(String[] args) //接收輸入參數,調用相關方法執行命令,匯總執行結果,輸出到指定文件或默認文件中
}
public class CommandDecoder {
public CommandDecoder()//構造方法
public Commands decode(String[] args) //輸入參數,進行分析之後返回Commands實例,該實例用於內部表示輸入的參數
private void getAllFilePath(String filePath, ArrayList<String> paths,String extension) //輸入文件夾地址、文件地址容器、指定後綴名,遞歸掃描指定文件夾中的所有文件
}
public class Processor {
private void init(ArrayList<Boolean> functions, String stopListPath) //輸入相關命令和stopList地址(若指定),初始化相關參數
ArrayList<String> process(String path, ArrayList<Boolean> functions, String stopListPath)//根據文件地址、需要分析的子功能、stopList對文件進行分析,返回分析結果
private void processWord(String rawLine) //輸入一行代碼,分析該行代碼中的單詞數,若指定stopList則與stopList對比
private void processCode(String rawLine)//分析輸入的一行代碼是空行、代碼行還是註釋行
}
public class Commands {
boolean c = false;//是否分析字符數,true則進行分析
boolean w = false;//是否分析單詞數
boolean l = false;//是否分析行數
boolean a = false;//是否對代碼行、空行以及註釋行進行統計
boolean s = false;//是否遞歸分析
String stopListPath = null;//stopList路徑,null表示不使用
String outputPath = null;//輸出文件路徑,null表示默認文件
ArrayList<String> filePath = new ArrayList<String>();//所有需要分析的文件路徑
}
上面的流程圖和方法註釋可以很清楚的了解程序的整體結構和設計實現過程,不再贅述。
代碼說明
上述說明已經給出了程序的實現思路,在代碼說明這一節,選取幾個比較復雜的函數和處理方法進行說明。這裏我們給出decode(String[] args)
、process(String path, ArrayList<Boolean> functions, String stopListPath)
、processCode(String rawLine)
和stopList的處理的部分代碼和分析。
首先是decode(String[] args)
public Commands decode(String[] args) {
//根據之前分析的命令特點,-o後面必跟輸出文件路徑,-e後面必跟stopList路徑,剩下的就是要分析的源代碼的路徑
String filePath = null;//命令中指定的原始路徑
Commands cmds= new Commands();//用來表示原始命令的內部實例,可以看作輸入命令的結構體
//for循環處理每個arg,用switch-case確定每個arg對應的操作。
for (int i = 0; i < args.length; i++) {
String cmd = args[i];
switch (cmd) {
case "-c":
cmds.c = true;
break;
case "-w":
cmds.w = true;
break;
case "-l":
cmds.l = true;
break;
case "-s":
cmds.s = true;
break;
case "-e":
cmds.stopListPath = System.getProperty("user.dir") + "\\" + args[++i];//獲得stopList的絕對路徑
break;
case "-o":
cmds.outputPath = args[++i];
break;
case "-a":
cmds.a = true;
break;
default:
filePath = args[i];
}
}
//若指定-s參數之後,需要遞歸遍歷目錄樹
if (cmds.s) {
if (filePath.contains("\\")) {
//處理C:\adb\*.c類似路徑
} else {
//處理*.txt類似路徑
}
} else {
//未指定時,獲得指定文件的絕對路徑
}
return cmds;
其次是process(String path, ArrayList<Boolean> functions, String stopListPath)
ArrayList<String> process(String path, ArrayList<Boolean> functions, String stopListPath) {
//由WordCount將命令進行分解,循環調用本方法進行處理。
//整體思路:從源代碼文件中循環讀取每一行,根據相應的flag進行處理,完成之後根據各個flag匯總處理結果
//變量初始化
init(functions, stopListPath);
BufferedReader bufferedReader = null;
//循環讀取每一行代碼並根據flag進行處理
try {
bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(path)));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
if (c) {
String tmpLine = line.replaceAll(" ", " ");
charCount += tmpLine.length() + 1;
}
if (w) {
processWord(line);
}
if (l) {
lineCount++;
}
if (a) {
processCode(line);
System.out.println(noteLineCount + " flag " + noteFlag);
}
}
lineCount--;//換行符糾正
//關閉流並進行try-catch-finally處理
bufferedReader.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bufferedReader.close();
bufferedReader = null;
} catch (Exception e) {
e.printStackTrace();
bufferedReader = null;
}
}
String[] tmp = path.split("\\\\");
String fileName = tmp[tmp.length - 1] + ",";//根據輸入的文件路徑獲取文件名
//根據flag匯總對當前文件的分析結果
ArrayList<String> result = new ArrayList<>();
if (c) {
result.add(fileName + "字符數: " + charCount);
}
if (w) {
result.add(fileName + "單詞數: " + wordCount);
}
if (l) {
result.add(fileName + "行數: " + lineCount);
}
if (a) {
result.add(fileName + "代碼行/空行/註釋行: " + String.format("%d/%d/%d", codeLineCount, emptyLineCouneLineCount));
}
return result;
}
接下來是processCode(String rawLine)
private void processCode(String rawLine){
//由於測試用例不一定遵循語法規範,這裏的代碼僅適用於課程測試用例
//此方法用來區分代碼行、註釋行、空行
/*簡單處理思路:
*空行:若去掉空格和制表符之後長度小於等於1則為空行;
*代碼行:若在/* * /中,則為註釋行,否則去掉空格和制表符之後是以下情況,則為註釋行,否則為代碼行
1.//開頭
2./*開頭,並且本行不包含* /或者本行以* /結尾或者以* /}結尾
3.{/*開頭,且與2類似
4.}/*且與2類似
5.{//開頭
6.}//開頭
7.本行為* /
8.本行為* /}
*/
String tmp = rawLine.replaceAll("[ \t]", "");
int index = 0;
while ((rawLine.charAt(index) + "").equals(" ")) {
index++;
}
tmp = rawLine.substring(index, rawLine.length());
System.out.println(tmp);
//長度小於等於1則為空行
if (tmp.length() <= 1) {
emptyLineCount++;
return;
}
//設置flag,用於判斷是否在多行註釋中
if (tmp.contains("/*")) {
noteFlag = true;
}
if (tmp.contains("*/")&¬eFlag) {
noteFlag = false;
}
if (noteFlag ) {//判斷是否在多行註釋之間
noteLineCount++;
return;
} else if (tmp.startsWith("//")//判斷是否在上述八種代碼行情況之中
|| (tmp.startsWith("/*")
&& (!tmp.contains("*/") || (tmp.endsWith("*/") || tmp.endsWith("*/}"))))
|| (tmp.startsWith("{/*")
&& (!tmp.contains("*/") || (tmp.endsWith("*/") || tmp.endsWith("*/}"))))
||(tmp.startsWith("}/*")
&& (!tmp.contains("*/") || (tmp.endsWith("*/") || tmp.endsWith("*/}"))))
|| tmp.startsWith("{//")
|| tmp.startsWith("}//")
||tmp.equals("*/")
||tmp.equals("*/}")) {
noteLineCount++;
return;
}
codeLineCount++;
}
最後是stopList的處理:
將stopList進行分割時候用HashMap<String,String>
進行存儲,可以實現快速查找。
測試設計過程
在線學習2.3節對判定的測試中指出“語句覆蓋就是要保證設計的測試用例應至少覆蓋函數中所有的可執行語句”,為此,我針對CommandDecoder類中的decode方法,使用語句覆蓋指標設計測試用例,共產生3個測試用例。
下面給出CommandDecoder的程序圖:
通過上面的程序圖可以看到,環復雜度為11(分析方法參見[1]),我們可以設計十一個測試用例來進行測試,但是實際上由於循環的存在,最多需要三個測試用例即可覆蓋全部路徑。通過上面我們對decode方法的分析,可以發現高風險部分在於,當需要遞歸處理文件時,能夠將所有該目錄及子目錄下所有匹配到的文件都找到,因此應特別關註需要遞歸處理時結果的正確性。
設計的3個測試用例及覆蓋的路徑如下表所示:
編號 | 用例 | 路徑覆蓋 |
---|---|---|
1 | -c -l -w test.txt -e stopList.txt -o output.txt | e1 e2 e10 e3 e11 e4 e12 e6 e14 e7 e15 e18 e20 e23 |
2 | -c -s *.c | e1 e2 e10 e4 e12 e18 e19 e22 e25 |
3 | -c -s C:\Users\testfile\*.c | e1 e2 e10 e4 e12 e18 e19 e21 e24 |
覆蓋率為100%
同樣的,我們對Processor類的processCode方法進行分析,也可以得到如下程序圖
環復雜度為12,由此設計了12個測試用例,由於篇幅原因下面只給出七個
編號 | 用例 | 覆蓋路徑 |
---|---|---|
1 | //aa | e1 e3 e5 e7 e10 e18 e9 |
2 | /*aa | e1 e3 e5 e7 e11 e19 e9 |
3 | {/*aa | e1 e3 e5 e7 e12 e20 e9 |
4 | }/*aa | e1 e3 e5 e7 e13 e21 e9 |
5 | {//aa | e1 e3 e5 e7 e14 e22 e9 |
6 | }//aa | e1 e3 e5 e7 e15 e23 e9 |
7 | */ | e1 e3 e5 e7 e16 e24 e9 |
路徑覆蓋58.3%
參考資料:
無
軟件開發及測試