隨筆記錄方便自己和同路人查閱。
#------------------------------------------------我是可恥的分割線-------------------------------------------
自動化測試用例設計
對於測試人員來說,不管是進行功能測試、自動化測試還是性能測試都需要編寫測試用例,測試用例的好壞往往能准確地體現測試人員的經驗、能力以及對項目需求的理解深度。所以,在正式開展自動化測試工作之前,我們有必要聊聊自動化測試用例的一些特點,以及如何編寫自動化測試用例。
手動測試用例與自動化測試用例
手工測試用例針對功能測試人員的,而自動化測試用例則是針對自動化測試框架或工具的;前者是功能測試用例人員通過手工方式進行用例解析,后者是應用腳本技術進行用例解析。兩者各自最大的特點在於,前者具有較好的異常處理能力,能夠基於測試用例,制造各種不同的邏輯判斷,而且人工測試步步跟蹤,能夠細致地定位問題;而后者是完全按照測試用例的步驟進行測試,只能在已知的步驟與場景中發現問題,而且往往因為網絡問題或功能的微小變化導致用例執行異常,自動化的執行也很難發現新的bug。
手工測試用例與自動化測試用例對比如下。
- 手工測試用例特點:
* 較好的異常處理能力,能通過人為的邏輯判斷校驗當前步驟的功能是否正確實現。
* 人工執行用例具有一定的步驟跳躍性。
* 人工測試步步跟蹤,能夠細致地定位問題。
* 主要用來發現功能缺陷。
- 自動糊測試用例特點
* 執行對象是腳本,任何一個判斷都需要編碼定義。
* 用例步驟之間關聯性強。
* 主要用來保證產品主體功能正確和完整,讓測試人員從繁瑣重復的工作中解脫出來。
* 目前自動化測試階段定位在冒煙測試和回歸測試。
通過對比我們可以看到,手工測試用例與自動化測試用例之間存在較大的差異,所以,不能直接把手工測試用例“翻譯”成自動化測試腳本。
通過它們之間的特點對比也可清晰地認識到,自動化測試不能完全地代替手工測試,自動化測試的目的僅僅在於讓測試人員從繁瑣重復的測試過程中解脫出來,把更多的時間和精力放到更有價值的測試中,例如探索性測試。而自動化測試更多的是用來進行冒煙測試和回歸測試。
- 自動化測試用例選型注意事項:
1)不是所有的手工用例都要轉為自動化測試用例。
2)考慮到腳本開發的成本,不要選擇流程太復雜的用例。如果有必要,可以考慮把流程拆分成多個用例來實現腳本。
3)選擇的用例最好可以構建成場景。例如,一個功能模塊,分多個用例,多個用例使用同一個場景。這樣的好處在於方便構建關鍵字測試模型。
4)選擇的用例可以帶有目的性。例如,這部分用例作為冒煙測試,那部分用例作為回歸測試等,當然,會存在重疊的關系。如果當前用例不能滿足需求,那么唯有修改用例來適應腳本和需求。
5)選取的用例可以是你認為是重復執行,很繁瑣的部分。例如,字段驗證、提示信息驗證這類,這部分適用於回歸測試。
6)選取的用例可以是主體流程,這部分適用於冒煙測試。
7)自動化測試也可以用來做配置檢查、數據庫檢查。這些可能超越了手工用例,但也算用例擴展的一部分,項目負責人也可以有選擇地增加。
8)平時在手工測試時,如果需要構建一些復雜的數據或重復一些簡單的機械式動作,則告訴自動化腳本,讓它來幫你,或許你的效率會因此而得到提高。
測試類型
- 測試靜態內容
靜態內容測試是最簡單的測試,用於驗證靜態的、不變化的UI元素的存在性。例如:
* 每個頁面都有其預期的頁面標題嗎?這可以用來驗證鏈接指向一個預期的頁面。
* 應用程序的主頁包含一個應該在頁面頂部的圖面嗎?
* 網站的每一個頁面是否都包含一個頁腳區域來顯示公司的聯系方式、隱私政策以及商標信息?
* 每一頁的標題文本都使用的<h1>標簽嗎?每個頁面都有正確的頭部文本嗎?
你可能需要(也可能不需要)對頁面內容進行自動化測試。如果你的頁面內容是不易受到影響,則手工對內容進行測試就足夠了。假設你的應用文件的位置被移動了,則內容測試就非常有價值。
- 測試連接
Web站點的一個常見錯誤為失效的鏈接或鏈接指向無效頁。鏈接測試涉及各個鏈接和驗證預期的頁面是否存在。如果靜態鏈接不經常更改,則手動測試就足夠了。但是,如果你的網頁設計師經常改鏈接,或者文件不時被重定向,則鏈接測試應該事先自動化。
- 功能測試
在你的應用程序中,需要測試應用的特點功能,需要一些類型的用戶輸入,並返回某種種類型的結果。通常一個功能測試將涉及多個頁面,一個基於表單的輸入頁面,其中包含若干輸入字段提交和取消操作,以及一個或多個響應頁面。用戶輸入可以通過文本輸入域、復選框、下拉列表,或任何其他瀏覽器所支持的輸入。
功能測試通常是需要自動化測試的最復雜的測試類型,但通常也是最最重要的。典型的測試是登錄、注冊網站賬戶、用戶賬戶操作、賬戶設置變化、復雜的數據檢索操作,等等。功能測試通常對應着你的應用程序的描述應用特性或設計的使用場景。
- 測試動態元素
通常一個網頁元素都有一個唯一的標識符,用於唯一地定位該網頁中的元素。通常情況下,唯一標識符用HTML標記的“id”屬性或“name”屬性來實現。
這些標識符可以是一個靜態的(既不變得)字符串常量,也可以是動態生成值,在每個頁面實例上都是變化的。例如,有些Web服務器可能在一個頁面實例上命名所顯示的文件為doc3861,而在其他頁面實例上顯示為doc6148,這個取決於用戶在檢索的“文檔”。驗證文件是否存在的測試腳本可能無法找到不變的識別碼來定位該文件。通常情況下,具有變化的標識符的動態元素存在於基於用戶操作的結果頁面上,然而,顯然這取決於Web應用程序。
- Ajax的測試
Ajax是一種支持以動態改變用戶界面元素的技術。頁面元素可以動態更改,單不需要瀏覽器重新載入頁面,如動畫、RSS源、其他實時數據更新等。Ajax有無數更新網頁上元素的方法。最簡單的方法是在Ajax驅動的應用程序中,數據可以從應用服務器檢索,然后顯示在頁面上,而不需要重新加載整個頁面,只有一下部分的頁面,或者只有元素本身被重新加載。
自動化測試用例編寫原則
在編寫自動化測試用例過程中應該遵循以下原則:
1)一個用例為一個完整的場景,從用戶登錄系統到最終退出並關閉瀏覽器。
2)一個用例只驗證一個功能點,不要試圖在用戶登錄系統后把所有的功能都驗證一遍。
3)盡可能少的編寫逆向邏輯用例。一方面因為逆向邏輯的用例很多(例如,手機號輸錯有幾十種情況);另一方面自動化腳本本身比較脆弱,復雜的逆向邏輯用例實現起來較為麻煩且容易出錯。
4)用例與用例之間盡量避免產生依賴。
5)一條用例完成測試之后需要對測試場景進行還遠,以免影響其他用例的執行。
BBS社區項目實戰
本篇以一個BBS社區項目為例,BBS社區屬於互聯網比較典型的應用,主要有登錄、個人中心、發帖、查看帖子、搜索、簽到等功能。
准備工作
- 項目開發是個循序漸進的過程
需要向讀者說明的是,我接下來要介紹的這個自動化測試項目,並非項目的最初的形態,其間經歷了多次代碼迭代與結構的重構,並且僅僅只符合當前的項目需求。為什么要強調這些呢?
相信我們都知道一個只有幾條測試用例的項目和有一個幾百條測試用例的項目結構肯定不一樣的。對於只有幾條測試用例的項目,我不需要考慮太多結構方面的問題,甚至只用線性模型來編寫用例,其維護成本也不會太高;但是,當用例達到幾百條時就不得不考慮各種問題,例如,如何降低測試代碼的冗余、對代碼進行抽象與分層、采用哪種設計模式,等等。
自動化測試的開發,是個不斷調整代碼與結構的過程,也許第一天你編寫了二十條用例,到第二天的時候,你需要花三分之一的時間對昨天的部分代碼進行調整或忠狗。只有三分之二的時間用於編寫新的用例。類、方法和函數的命名也是需要考究的方面,既要盡量保持簡潔,又要見名知意,代碼的編寫更是如此,如何寫出簡潔優雅的代碼是對我們變成功底的考驗。遺憾的是無法帶着讀者去復盤這樣一個過程。其實,這個過程也必須由讀者自己在不斷實踐中積累和總結。
- 選擇合適的IDE
工欲善其事,必先利其器,再開始開發自動化項目之前,我們有必要先來聊一聊Python有哪些IDE。當然關於IDE的討論一直屬於熱門話題,這個不是要分辨個孰優孰劣,這里只是想告訴讀者不同的編程階段應選擇適合自己的IDE。
Python IDLE:如果讀者初學Python,並且不精通其他變成語言及IDE,則建議從這個IED入手,它自帶的Shell模式可以幫助我們快速連寫Python語法,筆者初學Python時用了半年。
UliPad:輕量級的Python IDE,有國內用戶基於wsPython開發,代碼着色及自動補全功能很不錯,配置也相對比較簡單。
Sublime:通用型輕量級IED,支持多種變成語言。有許多功能強大的快捷鍵(Ctrl+d),如果平時需要在多種編程語言間切換,name這將是不錯的選擇。這也是筆者最常用的IDE之一。
PyCharm:Python 重量級IDE,功能強大,自動檢測語法,可以幫助我們寫出更規范的Python代碼。對於處女座的開發者來說是個不錯的選擇。
Eclipse + pydev:Eclipse也屬於重量級IDE。相信學習Java語言的同學一般都會選擇此IDE,配置pydev插件后同樣可以用來編寫Python程序,對於熟悉Eclipse的同學是個不錯的選擇。
Vim與Emacs:一直是程序員大神口中的神器,學習成本很高。
通過簡單介紹,相信讀者已經找到了適合自己的IED,下面就跟着筆者一起動手開發自動化項目吧。
項目結構介紹
自動化測試項目結構如下圖:

下面逐級介紹此目錄與文件的作用:

1.mztestpro測試項目
BBS:用於存放BBS項目的測試用例、測試報告和測試數據等。
driver:用於存放瀏覽器驅動。如selenium-server-standalone-2.47.0.jar、chromedriver.exe、IEDriverServer.exe等。在執行測試前根據執行場景將瀏覽器驅動復制到系統環境變量path目錄下。
package:用於存放自動化所用到的擴展包。例如,HTMLTestRunner.py屬於一個單獨模塊,並且對其做了修改,所以,在執行測試前需要將它復制到Python的Lib目錄下。
run_bbs_test.py:項目主程序。用來運行社區(BBS)自動化用例。
start.bat:用於啟動Selenium Server,默認啟動driver目錄下的selenium-server-standalone-2.47.0.jar。
自動化測試項目說明文檔.docx:介紹當前項目的架構、配置和使用說明。
2.bbs目錄
data:該目錄用來存放測試相關的數據。
report:用於存放HTML測試報告。其下面創建了image目錄用於存放測試過程中的截圖。
test_case:測試用例目錄,用於存放測試用例及相關模塊。
3.test_case目錄
models:該目錄下存放了一些公共的配置函數及公共類。
page_obj:該目錄用於存放測試用例的頁面對象(Page Object)。根據自定義規則,以“*Page.py”命名的文件為封裝的頁面對象文件。
“*_sta.py”:測試用例文件。根據測試文件匹配規則,以“*_sta.py”命名的文件將被當做自動化測試用例執行。
編寫公共模塊
首先定義驅動文件
..\mztestpro\BBS\test_case\models\driver.py
# !/usr/bin/env python # -*- coding: UTF-8 –*- __author__ = 'Mr.Li' from selenium.webdriver import Remote from selenium import webdriver #啟動瀏覽器驅動 def browser(): #driver = webdriver.Chrome() host = '127.0.0.1:44444'#運行主機:端口號(本機默認:127.0.0.1:44444) dc = {'browserName':'chrome'}#指定瀏覽器('chrome','firefox') driver = Remote(command_executor='http://' + host + '/wd/hub', desired_capabilities=dc) return driver if __name__ == '__main__': dr = browser() dr.get('http://www.baidu.com') dr.quit()
定義瀏覽器驅動函數browser(),該函數可以進行設置,根據我們的需求,配置測試用例在不同的主機及瀏覽器下運行。如果不知道如何配置,可以看前面關於Selenium Grid2的文章。
自定義測試框架:
..\mztestpro\BBS\test_case\models\myunit.py
# !/usr/bin/env python # -*- coding: UTF-8 –*- __author__ = 'Mr.Li' from .driver import browser import unittest class MyTest(unittest.TestCase): def setUp(self): self.driver = browser() self.driver.implicitly_wait(10) self.driver.maximize_window() def tearDown(self): self.driver.quit()
定義MyTest()類用於繼承unittest.TestCase類,因為筆者創建的所有測試類中setUp()與tearDown()方法所做的事情相同,所以,將他們抽象為MyTest()類,好處就是在編寫測試用例時不再考慮這兩個方法的實現。
定義截圖函數:
..\mztestpro\BBS\test_case\models\function.py
# !/usr/bin/env python # -*- coding: UTF-8 –*- __author__ = 'Mr.Li' from selenium import webdriver import os #截圖函數 def insert_img(driver,file_name): base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) file_path = base_dir + "/report/image/" + file_name driver.get_screenshot_as_file(file_path) print('in the insert_img') if __name__ == '__main__': driver = webdriver.Chrome() driver.get('http://www.baidu.com') insert_img(driver,'baidu.png') driver.quit()
創建截圖函數insert_img(),為了保持自動化項目的移植性,采用相對路徑的方式將測試截圖保存到.\report\image\目錄中。
編寫Page Object
關於Page Object設計模式,在前面文章關於“自動化測試高級應用”篇中有過介紹,這里我們將使用該設計模式來編寫測試用例。
首先創建基礎Page基礎類:
..\mztestpro\BBS\test_case\page_obj\base.py
# !/usr/bin/env python # -*- coding: UTF-8 –*- __author__ = 'Mr.Li' class Page(object): ''' 頁面基礎類,用於所有頁面的繼承 ''' bbs_url = 'http://bbs.meizu.cn' def __init__(self,selenium_driver,base_url=bbs_url,parent=None): self.base_url = base_url self.driver = selenium_driver self.parent = parent def _open(self,url): url = self.base_url + url self.driver.get(url) assert (self.on_page(),'Did not land on %s' % url) def find_element(self,*loc): return self.driver.find_element(*loc) def find_elements(self,*loc): return .self.driver.find_elements(*loc) def open(self): self._open(self.url) def on_page(self): return self.driver.current_url == (self.base_url + self.url) def script(self,src): return self.driver.execute_script(src)
創建頁面基礎類,通過__init__()方法初始化參數:瀏覽器驅動、URL地址、超時市場等。定義基本方法:open()用於打開BBS地址:find_elemnet()和find_elemnets()分別用來定位單個與多個元素;創建scrip()方法可以更簡單地調用JavaScript代碼。當然我們還可以對更多的WebDriver方法進行重定義。
創建BBS登錄對象類:
..\mztestpro\BBS\test_case\page_obj\loginPage.py
# !/usr/bin/env python # -*- coding: UTF-8 –*- __author__ = 'Mr.Li' from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from .base import Page from time import sleep class login(Page) ''' 用戶登錄頁面 ''' url = '/' #Action bbs_login_user_loc = (By.XPATH,"//div[@id='mzCust]/div/img") bbs_login_button_loc = (By.ID,"mzLogin") def bbs_login(self): self.find_element(*self.bbs_login_user_loc).click() sleep(1) self.find_element(*self.bbs_login_button_loc).click() login_username_loc = (By.ID,"account") login_password_loc = (By.ID,"password") login_button = (By.ID,"login") #登錄用戶名 def login_username(self,username): self.find_element(*self.login_username_loc).send_keys(username) #登錄密碼 def login_password(self,password): self.find_element(*self.login_password_loc).send_keys(password) #登錄按鈕 def login_button(self): self.find_element(*self.login_button_loc).click() #定義同意登錄入口 def user_login(self,username="username",password='1111'): '''獲取的用戶名密碼登錄''' self.open() self.bbs_login() self.login_username(username) self.login_password(password) self.login_button() sleep(1) user_error_hint_loc = (By.XPATH,"//span[@for='account']") pawd_error_hint_loc = (By.XPATH,"//span[@for='password']") user_login_success_loc = (By.ID,"mzCustName") #用戶名錯誤提示 def user_error_hint(self): return self.find_element(*self.user_error_hint_loc).text #密碼錯誤提示 def pawd_error_hint(self): return self.find_element(*self.pawd_error_hint_loc).text #登錄成功用戶名 def user_login_success(self): return self.find_element(*self.user_login_success_loc).text
創建登錄頁面對象,對用戶登錄頁面上的用戶名/密碼輸入框、登錄按鈕和提示信息等元素的定位進行封裝。除此之外,還創建user_login()方法作為系統統一登錄的入口。關於對操作步驟的封裝既可以放在Page Object當中,也可以放在測試用例當中,這個主要根據具體需求來衡量。這里之所以放在Page Object當中,主要考慮到還有其他用例會調用到該登錄方法。為username和password入參設置了默認值為了方便其他用例在調用時也方便了在賬號失效時的修改。
編寫測試用例
現在開始編寫測試用例程序,因為前面已經做好了基礎工作,此時測試用例的編寫將會簡便很多,更能幾種精力考慮用例的設計與實現。
創建BBS登錄類:
..\mztestpro\BBS\test_case\login_sta.py
此處需要注意文件名的創建。例如,假設登錄頁的對象命名為loginPage.py,那么關於測試登錄的用例文件應該命名為login_sta.py這樣方便后期用例報錯時問題的追蹤。盡量把一個頁面上的元素定位封裝到一個“*Page.py”文件中,把針對這個頁面的測試用例集中到一個“*_sta.py”文件中。
# !/usr/bin/env python # -*- coding: UTF-8 –*- __author__ = 'Mr.Li' from time import sleep import unittest,random,sys sys.path.append("./models") sys.path.append(",.page_obj") from models import myunit,function from page_obj.loginPage import login class loginTest(myunit.MyTest): '''社區登錄測試''' #測試用戶登錄 def user_login_verify(self,username="",password=""): login(self.driver).user_login(username,password) def test_login(self): '''用戶名,密碼為空登錄''' self.user_login_verify() po = login(self.driver) self.asserEqual(po.user_error_hint(),"賬號不能為空") self.asserEqual(po.pawd_eeror_hint(),"密碼不能為空") function.insert_img(self.driver,"user_pawd_empty.jpg") def test_login2(self): '''用戶名正確,密碼為空登錄''' self.user_login_verify(username="pytest") po = login(self.driver) self.asserEqual(po.pawd_eeror_hint(), "密碼不能為空") function.insert_img(self.driver, "pawd_empty.jpg") def test_login3(self): '''用戶名為空,密碼正確''' self.user_login_verify(password="abc123456") po = login(self.driver) self.asserEqual(po.user_eeror_hint(), "賬號不能為空") function.insert_img(self.driver, "user_empty.jpg") def test_login4(self): '''用戶名與密碼不匹配''' character = random.choice('zyxwvutsrqponmlkjihgfedcba') username = "zhangsan" + character self.user_login_verify(username=username,password='123456') po = login(self.driver) self.asserEqual(po.pawd_eeror_hint(),"密碼與賬號不匹配") function.insert_img(self.driver,"user_pawd_error.jpg") def test_login5(self): '''用戶名、密碼正確''' self.user_login_verify(username="zhangsan",password="123456") sleep(2) po = login(self.driver) self.asserEqual(po.user_login_success(), "張三") function.insert_img(self.driver, "user_pawd_true.jpg") if __name__ == '__main__': unittest.main()
首先創建loginTest()類,繼承myunit.MyTest()類,關於MyTest()類的實現,請翻看前面的代碼。這樣就省卻了在每個測試類中實現一遍setUp()和tearDown()方法。
創建user_login_verify()方法,並調用loginPage.py中定義的user_login()方法。為什么不直接調用呢?因為user_login()的入參已經設置了默認值,原因前面已經解釋,這里需要重新將其入參的默認值設置為空即可。
前三條測試用例很好理解,分別驗證:
* 用戶名密碼為空,點擊登錄;
* 用戶名正確,密碼為空,點擊登錄;
* 用戶名為空,密碼正確,點擊登錄。
第四條用例驗證錯誤的用戶名和密碼登錄。在當前系統中如果反復使用固定且錯誤的用戶名和密碼,系統會彈出驗證碼輸入框。為了避免這種情況的發生,這需要用戶名進行隨機變化,此處的做法用固定的前綴“zhangsan”,末尾字符從a~z中隨機一個字符與前綴進行拼接。
第五條用例驗證正確的用戶名和密碼登錄,通過獲取用戶名作為斷言信息。
在上面的測試用例中,每條測試用例結束時都調用function.py文件中的insert_img()函數進行截圖。當用例運行完成后,打開.../report/image/目錄將會看到用例執行的截圖文件,如下圖:
執行測試用例
為了在測試用例運行過程中不影響做其他事,筆者選擇調用遠程主機或虛擬機來運行測試用例,那么這里就需要使用Selenium Grid(其包含在Selenium Server)來調用遠程節點。
創建..\mztestpro\startup.bat文件,用於啟動..\mztestpro\driver\目錄下的Selenium Server。
java -jar ./driver/selenium-server-standalone-3.141.59.jar -role hub
雙擊strtup.bat文件,啟動Selenium Server創建hub節點。在遠程主機或虛擬機中同樣需要啟動Selenium Server創建node節點,創建方式參見前面關於Selenium Grid的文章。
創建用例執行程序:...\mztestpro\run_bbs_test.py
# !/usr/bin/env python # -*- coding: UTF-8 –*- __author__ = 'Mr.Li' from HTMLTestRunner import HTMLTestRunner from email.mime.text import MIMEText from email.header import Header import smtplib import unittest import time import os def send_mail(file_new): f = open(file_new,'rb') mail_bobd = f.read() f.close() msg = MIMEText(mail_bobd,'html','utf-8') msg['Subject'] = Header('自動化測試報告','utf-8') smtp = smtplib.SMTP() smtp.connect("smtp.qq.com") smtp.login('username@qq.com','授權碼')#這個位置需要輸入授權碼而不是密碼 smtp.sendmail('username@qq.com','receive@qq.com',msg.as_string()) smtp.quit() print('email has send out!') # ==== 查找測試報告目錄,找到最新生成的測試報告文件 ==== def new_report(testreport): lists = os.listdir(testreport) lists.sort(key=lambda fn: os.path.getmtime(testreport + '\\' + fn)) file_new = os.path.join(testreport, lists[-1]) print(file_new) return file_new if __name__ == '__main__': now = time.strftime("%Y-%m-%d %H_%M_%S") filename = './report/' + now + 'result.html' fp = open(filename,'wb') runner = HTMLTestRunner(stream=fp, title="魅族社區自動化測試報告", description="環境:Windows10 瀏覽器:Chrome") discover = unittest.defaultTestLoader.discover('./test_case', pattern='*_sta.py') runner.run(discover) fp.close()#關閉生成的報告 file_path = new_report('./report/')#查找新生成的報告 send_mail(file_path) #調用發郵件模塊
執行過程中沒有做任何改動,繼承了HTMLTestRunner生成HTML測試報告,以及集成自動發郵件功能等。唯一需要注意的是,腳本中的路徑建議使用相對路徑,以便於項目被移動到任意目錄下執行。
打開...\models\driver.py文件,修改腳本運行的節點及瀏覽器。現在可以通過運行run_bbs_test.py來執行測試項目了。
