在實際的軟件開發項目中,經常要實現多個模塊之間的通信,這就需要大家約定好相互之間的通信協議,各自按照協議來收發和解析消息。
本文以實際的程序代碼為例,詳細介紹了如何用C語言來實現通信協議,並基於對協議字段的判斷,說明了程序單元測試的過程,為相關的開發工作提供了有益的參考。
一、軟件模塊之間的協議
什麼是軟件模塊之間的協議?不同的軟件模塊之間要實現相互通信,就必須遵循共同的消息規范,大家按照約定好的規范來收發消息。軟件模塊之間的協議就是不同模塊間消息交互的規范。
在通信協議中,一條完整的消息由消息頭和消息體構成,如圖1所示。
圖1 一條完整的消息示意圖
在C語言中,用結構體來表示協議。在進行消息解析的時候,一般只關注消息體的內容。消息頭只是用於標識一條消息,讓其它模塊能夠識別該類消息。
二、單元測試
在提交程序版本之前,開發人員需要對代碼進行單元測試和集成測試。那麼什麼是單元測試呢?單元測試就是對程序中的一個函數進行測試,看對於某個輸入,是否有預期的輸出。
單元測試的示意圖如圖2所示。
圖2 單元測試的示意圖
可以把函數看成一個灰色的盒子,測試的時候只關心輸入和輸出,要設計多組單元測試數據來對函數的功能進行測試。
此外,在測試中,還有一個叫做“測試用例”的概念。測試用例就是一次測試的整個過程,包括:測試目的、預置條件、測試步驟、預期結果、通過准則、測試工具等。
三、本程序中的協議
本程序中的協議包括了消息頭和消息體,其中,消息頭有四個字段,消息體有五個字段。如下代碼所示。
// 消息頭結構 typedef struct { UINT16 iReserve1; UINT16 iReserve2; UINT16 iReserve3; UINT16 iReserve4; }MsgHead_T; // 消息結構體(包含消息頭和消息體) typedef struct { MsgHead_T MsgHead; // 消息頭 UINT32 iOperType; // 操作類型 UINT8 szUserNumber[30]; // 用戶號碼 UINT8 szOperTime[20]; // 操作時間, 格式為: yyyymmdd UINT32 iReserve1; // 保留字段1 UINT8 szReserve2[50]; // 保留字段2 }UserReqMsg_T;
在消息體的五個字段中,操作類型、用戶號碼和操作時間是本次要進行判斷處理的字段,另外兩個字段是保留字段,可以先不用賦具體的值。
在協議中,為什麼要留有保留字段呢?這是方便以後對協議進行擴展。也就是說,如果以後除了操作類型、用戶號碼和操作時間之外,還需要增加新的字段定義,可以直接利用擴展字段。這在實際的軟件開發項目中是很重要的。
四、程序代碼
基於以上協議,本文中的程序代碼如下所示:
* 修改記錄1:// 修改歷史記錄, 包括修改日期、版本號、修改人及修改內容 * 修改日期: * 版本號: * 修改人: * 修改內容: * **********************************************************************/ #include <stdio.h> #include <string.h> // 重定義數據類型 typedef unsigned char UINT8; typedef unsigned short int UINT16; typedef unsigned int UINT32; typedef signed int INT32; // 消息頭結構 typedef struct { UINT16 iReserve1; UINT16 iReserve2; UINT16 iReserve3; UINT16 iReserve4; }MsgHead_T; // 消息結構體(包含消息頭和消息體) typedef struct { MsgHead_T MsgHead; // 消息頭 UINT32 iOperType; // 操作類型, 操作類型只能為1或2 UINT8 szUserNumber[30]; // 用戶號碼 UINT8 szOperTime[20]; // 操作時間, 格式為: yyyymmdd UINT32 iReserve1; // 保留字段1 UINT8 szReserve2[50]; // 保留字段2 }UserReqMsg_T; // 函數聲明 INT32 ProcUserReqMsg(UserReqMsg_T *ptUserReqMsg); INT32 main(); /********************************************************************** * 功能描述:主函數 * 輸入參數:無 * 輸出參數:無 * 返回值: 0-執行完畢 * 其它說明:無 * 修改日期 版本號 修改人 修改內容 * -------------------------------------------------------------------------------------------------- * 20140507 V1.0 zzx 創建 ***********************************************************************/ INT32 main() { UINT8 iRetVal = 0; UINT32 iOperType = 0; // 操作類型 UINT8 szUserNumber[30] = {0}; // 用戶號碼 UINT8 szOperTime[10] = {0}; // 操作時間, 格式為: yyyymmdd UserReqMsg_T tUserReqMsg = {0}; // 請求消息 // 對消息頭部進行賦值 tUserReqMsg.MsgHead.iReserve1 = 1; tUserReqMsg.MsgHead.iReserve2 = 2; tUserReqMsg.MsgHead.iReserve3 = 3; tUserReqMsg.MsgHead.iReserve4 = 4; // 讀入具體消息字段的值 printf("操作類型: \n"); scanf("%d", &iOperType); printf("用戶號碼: \n"); scanf("%s", szUserNumber); printf("操作時間: \n"); scanf("%s", szOperTime); // 對具體消息字段進行賦值(保留字段可不賦值) tUserReqMsg.iOperType = iOperType; strncpy(tUserReqMsg.szUserNumber, szUserNumber, strlen(szUserNumber));// 獲取號碼, 用strncpy代替strcpy strncpy(tUserReqMsg.szOperTime, szOperTime, strlen(szOperTime)); // 獲取時間, 用strncpy代替strcpy // 對消息體的字段進行異常判斷 iRetVal = ProcUserReqMsg(&tUserReqMsg); // 注意: 傳遞參數的時候要加上& if (iRetVal == 0) // 函數執行正確 { // 打印消息字段內容 printf("The user request message is: iOperType=%d, szUserNumber=%s, szOperTime=%s.\n", tUserReqMsg.iOperType, tUserReqMsg.szUserNumber, tUserReqMsg.szOperTime); return 0; } else // 打印異常消息 { printf("Some content of the user request message is wrong, please check!\n"); return -1; } }
/********************************************************************** * 功能描述:對消息體的字段進行異常判斷 * 輸入參數: ptUserReqMsg-用戶請求消息 * 輸出參數:無 * 返回值: 0-成功 其它-失敗 * 其它說明:無 * 修改日期 版本號 修改人 修改內容 * -------------------------------------------------------------------------------------------------- * 20140507 V1.0 zzx 創建 ***********************************************************************/ INT32 ProcUserReqMsg(UserReqMsg_T *ptUserReqMsg) { INT32 iRetValue = 0; // 對輸入參數進行異常判斷 if (ptUserReqMsg == NULL) { printf("ProcUserReqMsg(...): input parameter(ptUserReqMsg) is NULL.\n"); return -1; } // 對消息體字段進行異常判斷 if ((ptUserReqMsg->iOperType != 1) && (ptUserReqMsg->iOperType != 2)) // 操作類型只能為1或2, 其它為數據異常 { printf("ProcUserReqMsg(...): the iOperType is wrong, iOperType=%d.\n", ptUserReqMsg->iOperType); return -2; } if (strlen(ptUserReqMsg->szUserNumber) != 8) // 用戶號碼異常, 長度8位才正確 { printf("ProcUserReqMsg(...): the szUserNumber is wrong.\n"); return -3; } if (strlen(ptUserReqMsg->szOperTime) != 8) // 操作時間異常, 長度8位才正確 { printf("ProcUserReqMsg(...): the szOperTime is wrong.\n"); return -4; } return 0; }
本程序要對ProcUserReqMsg函數進行單元測試,看該函數能否對消息體的字段進行異常判斷。
五、單元測試用例
1. 正常測試用例
正常測試用例是指滿足程序輸入條件的測試用例,即觀察程序在正確的輸入情況下,能否產生正確的輸出。
什麼是正常測試?包括了兩種情況:
1) 輸入正確的值,程序產生正確的輸出。
2) 輸入錯誤的值,程序產生錯誤的輸出。
(1) “操作類型”為1
設定“操作類型”為1,“用戶號碼”和“操作時間”字段均符合協議要求。程序的執行情況如圖3所示。
圖3 “操作類型”為1的正常執行情況
(2) “操作類型”為2
設定“操作類型”為2,“用戶號碼”和“操作時間”字段均符合協議要求。程序的執行情況如圖4所示。
圖4 “操作類型”為2的正常執行情況
2. 異常測試用例
異常測試用例是指不滿足程序輸入條件的測試用例,即觀察程序在錯誤的輸入情況下,產生的結果是怎樣的。
什麼是異常測試?包括了兩種情況:
1) 輸入正確的值,程序產生錯誤的輸出。
2) 輸入錯誤的值,程序產生正確的輸出。
(1) “操作類型”不為1或2
設定“操作類型”為3(不為1或2的正整數),“用戶號碼”和“操作時間”字段均符合協議要求。程序的執行情況如圖5所示。
圖5 “操作類型”為3的異常執行情況
(2) “用戶號碼”不是8位
設定“操作類型”為1,“用戶號碼”字段為9位,“操作時間”字段符合協議要求。程序的執行情況如圖6所示。
圖6 “用戶號碼”為9位的異常執行情況
(3) “操作時間”不是8位
設定“操作類型”為1,“用戶號碼”符合協議要求,“操作時間”字段為9位。程序的執行情況如圖7所示。
圖7 “操作時間”為9位的異常執行情況
正常和異常測試的情況都有很多種,這裡就不一一列舉了。為了確保程序的正確性,一定要對程序(或者函數)進行充分的單元測試。
七、總結
對於協議,這是不同模塊之間通信的橋梁。因此,在開始編碼之前,一定要將協議定義清楚,這樣也可以減少後續修改帶來的不便。
對於單元測試,這是每個軟件開發工程師都必須要認真對待的。單元測試進行得是否徹底,會直接影響到軟件產品的質量。
本文以實際的程序代碼為例子,對用C語言表示協議和對代碼進行單元測試作了詳細的介紹。文中涉及到的協議表示方法和單元測試方法可供相關的軟件開發工程師參考。