從程式碼庫遷出程式碼 —- pexpect 的使用
測試人員從程式碼庫(例如 CVS )遷出程式碼的過程中,需要手動輸入訪問密碼,而 Python 提供了 Pexpect 模組則能夠將手動輸入密碼這一過程自動化。當然 Pexpect 也可以用來和 ssh、ftp、passwd、telnet 等命令列進行自動化互動。這裡我們以 CVS 為例展示如何利用 Pexpect 從程式碼庫遷出程式碼。
清單 1. 用 pexpect 遷出程式碼庫程式碼
1 2 3 4 5 6 7 8 |
try: chkout_cmd = 'cvs co project_code' #從程式碼庫遷出 project_code 的內容 child = pexpect.spawn(chkout_cmd) child.expect('password:') child.sendline('your-password') #請替換"your-password"為真實密碼 child.interact() except: pass #忽略遷出程式碼中的錯誤 |
在清單 1 中,我們用命令”cvs co project_code”從程式碼庫中遷出了 project_code 的內容,我們也可以用該命令來更新已經遷出的程式碼。只需要將命令”cvs update” 傳給類 pexpect.spawn()即可,詳細的實現請參考程式碼檔案。這裡 interact()函式是必須的,用來在互動的方式下控制該子程式。有時程式碼庫中會存在目錄不一致行情況,遷出程式碼會因報錯終止,所以需要異常處理(try … execpt)來忽略該錯誤。
編譯程式碼和執行測試指令碼 —- subprocess 的使用
測試人員獲取最新的程式碼之後,就要對原始碼進行編譯,並且執行測試用例。Python 語言提供了多種方法如 os.system()/os.popen()來執行一條命令,這裡我們推薦用 subprocess 模組來建立子程式,完成程式碼編譯和執行測試用例。因為 subprocess 支援主程式和子程式的互動,同時也支援主程式和子程式是同步執行還是非同步執行。由於本文中的各個功能模組有都先後依賴關係,所以全部採用的是主程式和子程式同步模式執行。
編譯程式碼
清單 2. 用 subprocess 編譯程式碼
1 2 3 4 |
build_cmd = 'build_command_for_your_code' #請在這裡配置編譯命令 build_proc = subprocess.Popen(build_cmd, stdin=None, stdout=None, stderr=None, shell=True) build_proc.wait() #等待子程式結束 assert (0 == build_proc.returncode) |
在一些系統中我們編譯程式碼採用的是指令碼檔案(如 shell 指令碼),那麼我們仍然可以如下命令來完成程式碼編譯工作。
清單 3. 用 subprocess 的 call 函式執行指令碼檔案
1 |
subprocess.call(["code_compile.sh"]) |
執行測試指令碼
在編譯完成程式碼之後,我們同樣可以呼叫 subprocess.Popen 來建立子程式執行測試用例。如果測試人員的測試用例已經寫成了測試例指令碼,我們則可以用 subprocess.call()來執行測試例指令碼檔案,程式碼實現就不再贅述。有些系統會直接把詳細日誌輸出到螢幕上,那麼我們可以用重定向命令”2>&1″把螢幕輸出寫檔案。
清單 4. 用重定向命令把輸出寫檔案
1 |
ut_cmd = 'Your_unit_test_command 2>&1 > %s' %self.debug_log #debug_log 定義在__init__函式中,用來儲存詳細日誌 |
測試結果儲存和釋出 —- XML 解析
我們的專案採用敏捷開發,為了更好的反應敏捷開發週期,我們希望儲存日誌的目錄名不但能夠指明的具體日期,同時也能反映敏捷(迭代)開發階段,這樣相關人員在檢視相應目錄中的日誌時,能夠清楚的明白日誌實在在哪個迭代週期的哪一天產生的。本文使用檔案 summary 作為執行測試用例後生成的彙總日誌,用檔案 log.txt 用來儲存詳細日誌。如下圖所示,在共享目錄 SharedFiles 中儲存了一些列迭代週期中的日誌。
清單 5. 共享目錄結構
1 2 3 4 5 6 7 8 |
SharedFiles ├── Sprint10-20130823121500 │ ├── log.txt │ └── summary ├── Sprint10-20130826152715 │ ├── log.txt │ └── summary ├── Sprint10-20130828165235 |
為了能夠讓目錄名反映敏捷開發週期,我們需要自己定義一個配置檔案(txt 或 xml 均可)。由於 Python 已經很好的支援了 XML 解析,並且 XML 檔案作為配置也是當前的流行趨勢。本文就以 XML 解析為例進行說明。本文使用的 XML 檔名是 Sprint.xml,清單 6 是該 xml 的概要內容
清單 6. Sprint.xml 檔案結構
1 2 3 4 5 6 7 8 9 |
<sprint-schedule> <min-sprint>10</min-sprint> <max-sprint>20</max-sprint> <sprint10>20130814</sprint10> <sprint11>20130828</sprint11> … … <sprint19>20131218</sprint19> <sprint20>20140101</sprint20> </sprint-schedule> |
關於 xml 解析 Python 提供了多種方法。本文采用 minidom 對 xml 檔案進行解析,清單 7 是相關處理程式碼。
清單 7. xml 解析程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
cur_date = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time())) # 首先獲取當前系統日期 xmldoc = minidom.parse(xml_file) min_num_node = xmldoc.getElementsByTagName('min-sprint')[0] min_num = int(min_num_node.firstChild.data) #解析出迭代開發週期的起始週期 max_num_node = xmldoc.getElementsByTagName('max-sprint')[0] max_num = int(max_num_node.firstChild.data) #解析出迭代開發週期的終止週期 cur_num = min_num #遍歷所有迭代週期,取出當前迭代週期的開始時間和當前的系統時間對比,從而確定當前位於哪一個迭代週期。 while cur_num <= max_num : node_name = 'sprint' + str(cur_num) cur_node = xmldoc.getElementsByTagName(node_name)[0] sprint_date = cur_node.firstChild.data if sprint_date < cur_date[0:7]: cur_num = cur_num + 1 else: break |
這樣 cur_num 就指向了當前的迭代開發週期。然後,我們就可以根據當前日期和開發階段建立對應的日誌目錄名了,最後把執行結果儲存到該目錄下,參見清單 8 實現。
清單 8. 日誌儲存程式碼
1 2 3 4 |
log_dir = self.share_dir + '/Sprint' + str(cur_num) + '-' + cur_date #share_dir 為共享目錄,定義在初始化函式中 os.mkdir(log_dir) os.system('mv %s %s' %(self.debug_fullname, log_dir)) #debug_fullname,詳細日誌檔名(含目錄),定義在初始化函式中 os.system('mv %s %s' %(self.sum_fullname, log_dir)) #sum_fullname,彙總日誌的全路徑檔名,定義在初始化函式中 |
關於測試結果的釋出,本文並沒有把測試結果以自動化的形式傳送郵件,而是手動在每個開發週期結束時,群發郵件給相關人員。或者在驗證失敗後,通知相關的開發人員,這是由於作者所在團隊專案程式碼提交頻率不是很高。在更大型的專案中,往往需要增加自動傳送郵件的功能,相關實現本文不再贅述。
也談介面設計 —- getopt 的使用
在日常的測試過程中,我們並不是每次都要遷出程式碼,編譯程式碼,執行測試用例和收集測試結果。這樣就需要我們能夠有選擇的執行部分程式功能,例如只執行測試用例和收集結果。這裡我們提供了 4 個執行選澤:
選項 1:遷出程式碼–>編譯版本–>執行測試用例–>收集測試結果
選項 2:更新程式碼–>編譯版本–>執行測試用例–>收集測試結果
選項 3:編譯版本–>執行測試用例–>收集測試結果
選項 4:執行測試用例–>收集測試結果
當然我們還需要提供幫助資訊,以方便不熟悉該指令碼實現的人員使用。python 也提供了 getopt 模組讓我們輕鬆實現上述功能。實現程式碼參見清單 9
清單 9. 命令列寫解析程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
try: opts, args = getopt.getopt(sys.argv[1:], 'bchu', ['build', 'checkout', 'help', 'update']) except getopt.error, msg: self.usage() sys.exit(2) build_flag = 0 #構建選項 for o, a in opts: if o in ('-h', '--help'): self.usage() sys.exit() elif o in ('-c', '--checkout'): print "執行操作:遷出程式碼-->編譯版本-->執行測試用例-->收集測試結果" build_flag = 1 break elif o in ('-u', '--update'): print "執行操作:更新程式碼-->編譯版本-->執行測試用例-->收集測試結果" build_flag = 2 break elif o in ('-b', '--build'): print "執行操作:編譯版本-->執行測試用例-->收集測試結果" build_flag = 3 break else: self.usage() sys.exit() if (0 == build_flag) : if 2 <= len(sys.argv): self.usage() sys.exit() raw_input('\n 按 Enter 鍵繼續。。。(Ctrl+C 退出)\t') if (1 == build_flag) : #遷出程式碼,並編譯程式碼 self.checkout_code() self.build_code() elif (2 == build_flag) : #更新程式碼,並編譯程式碼 self.update_code() self.build_code() elif (3 == build_flag) : #編譯程式碼 self.build_code() #執行測試用例並收集執行結果 self.set_python() self.run_testsuite() self.store_logs() |
如果我們在執行的過程中想中斷(如利用 Ctrl+C)一鍵迴歸測試程式的執行時,有時我們會發現雖然主程式已經被終止,但子程式仍在執行。我們能否在中斷主程式的同時也中斷子程式呢?答案當然是肯定的,我們可以用訊號處理函式捕獲訊號(如捕獲 Ctrl+C 產生的中斷訊號),然後在顯式終止對應的子程式。這裡就需要我們在建立子程式的時候,先儲存子程式 ID,當然把子程式 ID 儲存到初始化函式中,是個不錯的選擇,清單 10 是相關實現。
清單 10. 訊號處理程式碼
1 2 3 4 5 |
# 終止子程式的執行 def handler(self, signum, frame): if (-1 != self.subproc_id) : #subproc_id 定義在初始化函式中,用來儲存當前子程式的 ID os.killpg(self.subproc_id, signal.SIGINT) sys.exit(-1) |
這裡我們需要在初始化函式中註冊要捕獲的訊號,並且建立成員變數用來儲存子程式的 ID,詳細實現請參見清單 11。
基於物件的設計 —- class 的使用
最後終於輪到 class 登場了,提到 class 我們就不能不談建構函式(初始化函式)和解構函式。之前我們多次提到初始化函式,初始化函式允許我們定義一些變數,這些變數在整個類物件的生存週期內均有效。由於本文沒有向系統申請資源,就再不定義解構函式了。
清單 11. 初始化處理程式碼
1 2 3 4 5 6 7 8 9 |
def __init__(self): signal.signal(signal.SIGINT, self.handler) #註冊需要捕獲的訊號量 self.myafs_dir = os.getenv('myafs') self.subproc_id = -1 #子程式 ID,用來在終止主程式時也同時終止子程式 self.debug_log = 'log.txt' #儲存詳細執行日誌的檔名 self.debug_fullname = os.getcwd() + os.sep + self.debug_log #全路徑檔名(假設產生在該目錄下) self.sum_log = 'summary' #儲存彙總日誌的檔名 self.sum_fullname = os.getcwd() + os.sep + self.sum_log #全路徑檔名(假設產生在當前目錄下) self.share_dir = self.utafs_dir + '/SharedFiles' #共享目錄檔名 |
通常我們不需要太關注設計風格,只要 Python 指令碼能完成我們的測試要求即可。對於較小的指令碼,幾條 Python 指令順序執行即可。為了模組功能複用和可讀性,我們通常會把功能模組封裝成函式。本文將實現的所有函式都封裝到一個類中,使得該指令碼更加一體化。
清單 12. class 框架結構程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class COneClickRegTest: #設定一些經常使用的變數,如當前工作目錄,日誌名稱、儲存路徑等 def __init__(self): #設定 python 環境變數,實現參見程式碼檔案 def set_python(self): #更新程式碼,實現參見程式碼檔案 def update_code(self): #遷出程式碼,實現參見第 2 章程式碼 def checkout_code(self): #編譯版本,實現參見清單 1 程式碼 def build_code(self): #執行測試集,實現參見程式碼檔案 def run_testsuite(self): #儲存執行結果,實現參見清單 7 和清單 8 程式碼 def store_logs(self): #訊號處理,實現參見清單 10 程式碼 def handler(self, signum, frame): #指令碼使用說明,實現參見程式碼檔案 def usage(self): #命令列解析以及執行對應的功能,實現參見清單 9 程式碼 def main(self): |
結束語
Python 語言是一個易學易用的指令碼語言,筆者沒有多久的 Python 開發經驗,不過其他語言有的功能在 Python 中大都可以找到對應的實現,這也是筆者能夠在很短的時間內完成該測試指令碼的原因。因此,筆者把該語言和使用該語言完成一鍵迴歸測試介紹給大家,希望對大家有所幫助。正像筆者說的其他語言有的功能在 Python 中大都可以找到對應的實現,同樣,如果大家對某一種特定的指令碼語言或者開發語言特別熟悉,也完全可以採用所熟悉的語言來完成一鍵迴歸測試的工作。
下載資源
- 示例程式碼 (sample.zip | 3KB)
相關主題
- Python 網站是關於 Python 所有內容的起始點,其中包括正式Python 文件
- 簡明 Python 教程為”A Byte of Python”的簡體中文譯本,無論您剛接觸電腦還是一個有經驗的程式設計師,本教程都有助您學習使用 Python 語言。
- Python 技術手冊(第 2 版)是一本全面介紹有關 Python 語言和 Python 程式開發專業知識的參考手冊。
- 在PyUnit 網站可以下載到最新的 PyUnit 軟體包,以及詳細的使用者手冊。
- 在 developerWorks Linux 專區尋找為 Linux 開發人員(包括 Linux 新手入門)準備的更多參考資料,查閱我們最受歡迎的文章和教程。
- 在 developerWorks 上查閱所有 Linux 技巧和 Linux 教程。