Selenium3與Python3實戰 Web自動化測試框架
一、項目實戰中PO模型的設計與封裝
一般將所有的元素、數據都放在代碼中,並不利於自動化代碼的維護。最好的方式是盡量把數據、頁面、操作進行分離開:PO設計模式
PO設計模式的優勢:
- PO提供了一種業務流程與頁面元素操作分離的模式,這使得測試代碼變得更加清晰。
- 頁面對象與用例分離,使得我們更好的復用對象。
- 可復用的頁面方法代碼會變得更加優化
- 更加有效的命名方式使得我們更加清晰的知道方法所操作的UI元素
1、使用PO模式實現注冊頁面封裝
1)關於配置文件
LocalElement.py:
[RegisterElement] user_email=id>register_email user_email_error=id>register_email-error user_name=id>register_nickname user_name_error=id>register_nickname-error password=id>register_password password_error=id>register_password-error code_image=id>getcode_num code_text=id>captcha_code code_text_error=id>captcha_code-error register_button=id>register-btn
setting.py:
import os base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 項目首路徑 codeerror_path = os.path.join(base_dir,'Image','codeerror.png') # 驗證碼錯誤圖片路徑 code_path = os.path.join(base_dir,'Image','code.png') # 驗證碼圖片保存路徑 config_ini_dir = os.path.join(base_dir,'setting','localElement.ini') # localElement.ini 配置文件路徑
2)read.ini.py :用於讀取LocalElement.py文件中的配置信息
from setting.setting import config_ini_dir import configparser class Read_Ini(object): # 初始化 def __init__(self,node = None): if node: self.node = node else: self.node = 'RegisterElement' # 配置文件中的某個節點 self.cf = self.load_ini() def load_ini(self): # 加載文件 cf = configparser.ConfigParser() # 使用 configparser模塊讀取配置文件信息 cf.read(config_ini_dir) # 配置文件所在路徑 return cf def get_value(self,key): # 獲取配置文件中key的value值 data = self.cf.get(self.node,key) return data
3)base包下新建find_element.py文件:通過read_ini.py文件獲取配置文件的信息,用於定位注冊頁面的目標元素
from util.read_ini import Read_Ini class FindElement(object): """獲取元素所在位置""" def __init__(self,driver): self.driver = driver def get_element(self,key): read_ini = Read_Ini() data = read_ini.get_value(key) by,value = data.split('>') try: if by == 'id': return self.driver.find_element_by_id(value) elif by == 'name': return self.driver.find_element_by_name(value) elif by == 'className': return self.driver.find_element_by_class_name(value) elif by == 'xpath': return self.driver.find_element_by_xpath(value) else: return self.driver.find_element_by_css(value) except Exception as e: # print("find_element錯誤信息:",e) return None
4)page包新建register_page.py文件: 通過find_element.py文件,獲取目標元素所在的位置
from base.find_element import FindElement class RegisterPage(object): """獲取元素所在位置""" def __init__(self,driver): self.fd = FindElement(driver) #獲取郵箱元素 def get_email_element(self): return self.fd.get_element("user_email") #獲取用戶名元素 def get_username_element(self): return self.fd.get_element("user_name") #獲取密碼元素 def get_password_element(self): return self.fd.get_element("password") #獲取驗證碼元素 def get_code_element(self): return self.fd.get_element("code_text") #獲取注冊按鈕元素 def get_button_element(self): return self.fd.get_element("register_button") #獲取郵箱錯誤元素 def get_email_error_element(self): return self.fd.get_element("user_email_error") #獲取用戶名錯誤元素 def get_name_error_element(self): return self.fd.get_element("user_name_error") #獲取密碼錯誤元素 def get_password_error_element(self): return self.fd.get_element("password_error") #獲取驗證碼錯誤元素 def get_code_error_element(self): return self.fd.get_element("code_text_error")
5)handle包下新建register_handle.py文件:結合register_page.py文件獲取目標元素位置,再自動輸入對應信息
#coding=utf-8 from page.register_page import RegisterPage from util.get_code_value import GetCode class RegisterHandle(object): """打開頁面后自動輸入相應信息""" def __init__(self,driver): self.driver = driver self.register_p = RegisterPage(self.driver) #輸入郵箱 def send_user_email(self,email): # self.loger.info("輸入的郵箱值是:"+email) self.register_p.get_email_element().send_keys(email) #輸入用戶名 def send_user_name(self,username): # self.loger.info("輸入的用戶名是:"+username) self.register_p.get_username_element().send_keys(username) #輸入密碼 def send_user_password(self,password): # self.loger.info("輸入的密碼是:"+password) self.register_p.get_password_element().send_keys(password) #輸入驗證碼 def send_user_code(self,file_name): get_code_text = GetCode(self.driver) code = get_code_text.code_online(file_name) self.register_p.get_code_element().send_keys(code) #獲取文字信息 def get_user_text(self,info,user_info): try:# 容錯處理 if info == 'user_email_error': text = self.register_p.get_email_error_element().text # 獲取郵箱錯誤信息 elif info == 'user_name_error': text = self.register_p.get_name_error_element().text # 獲取用戶名錯誤信息 elif info == 'password_error': text = self.register_p.get_password_error_element().text # 獲取用戶密碼錯誤信息 else: text = self.register_p.get_code_error_element().text # 獲取驗證碼錯誤信息 except: text = None return text #點擊注冊按鈕 def click_register_button(self): self.register_p.get_button_element().click() #獲取注冊按鈕文字 def get_register_btn_text(self): """如獲取不到信息,表明頁面已成功跳轉""" return self.register_p.get_button_element().text
6)business包新建register_business.py文件:測試注冊頁面form表單功能邏輯
from handle.register_handle import RegisterHandle class RegisterBusiness: """測試注冊頁面form表單功能情況""" def __init__(self,driver): self.register_h = RegisterHandle(driver) def user_base(self,email,name,password,file_name): self.register_h.send_user_email(email) self.register_h.send_user_name(name) self.register_h.send_user_password(password) self.register_h.send_user_code(file_name) self.register_h.click_register_button() def register_succes(self): if self.register_h.get_register_btn_text() == None: # 注冊成功 return True else: return False # 郵箱錯誤 def login_email_error(self,email,name,password,file_name): self.user_base(email,name,password,file_name) if self.register_h.get_user_text('user_email_error',"請輸入有效的電子郵件地址") == None: #print("無錯誤,郵箱檢驗不成功") return True else: return False def login_name_error(self,email,name,password,file_name): self.user_base(email,name,password,file_name) if self.register_h.get_user_text('user_name_error',"字符長度必須大於等於4,一個中文字算2個字符") == None: #print("用戶名檢驗不成功") return True else: return False # 密碼錯誤 def login_password_error(self,email,name,password,file_name): self.user_base(email,name,password,file_name) if self.register_h.get_user_text('password_error',"最少需要輸入 5 個字符") == None: #print("密碼檢驗不成功") return True else: return False # 驗證碼錯誤 def login_code_error(self,email,name,password,file_name): self.user_base(email,name,password,file_name) if self.register_h.get_user_text('code_text_error',"驗證碼錯誤") == None: #print("驗證碼檢驗不成功") return True else: return False
7)case包新建first_case.py:測試注冊頁面form表單功能
from selenium import webdriver from setting import setting from business.register_business import RegisterBusiness class FirstCase(object): def __init__(self, file_name, url): self.driver = webdriver.Chrome() self.driver.get(url) self.driver.maximize_window() self.file_name = file_name self.login = RegisterBusiness(self.driver) def test_login_success(self): login_success = self.login.register_succes() if login_success: print("注冊成功,case調試失敗!") def test_login_email_error(self): email_error = self.login.login_email_error('11111@qq.com','aaaa','111111',self.file_name) if email_error == True: print("Error: email沒有錯誤提示,此條case執行失敗!") def test_login_username_error(self): self.driver.refresh() username_error = self.login.login_name_error('2222222@qq.com','bbbbb','111111',self.file_name) if username_error == True: print("Error: username沒有錯誤提示,此條case執行失敗!") def test_login_password_error(self): self.driver.refresh() password_error = self.login.login_password_error('333333@qq.com','ccccc','111111',self.file_name) if password_error == True: print("Error: password沒有錯誤提示,此條case執行失敗!") def test_login_code_error(self): self.driver.refresh() code_error = self.login.login_code_error('44444444@qq.com','dddddddd','111111',self.file_name) if code_error == True: print("Error: password沒有錯誤提示,此條case執行失敗!") def main_run(self): self.test_login_email_error() self.test_login_username_error() self.test_login_password_error() self.test_login_code_error() self.driver.close() if __name__ == '__main__': file_name = setting.code_path url = 'http://www.5itest.cn/register' first_case = FirstCase(file_name,url) first_case.main_run()
代碼遞進方向:
read.ini.py → find_element.py → register_page.py → register_handle.py → register_business.py → first_case.py
附例:
get_code_value.py:獲取注冊頁面圖片,截取驗證碼圖片部分區域,識別驗證碼圖片,獲取驗證碼內容

from PIL import Image from setting.ShowapiRequest import ShowapiRequest import time class GetCode: """獲取驗證碼圖片,解析驗證碼圖片並返回驗證碼值""" def __init__(self, driver): self.driver = driver def get_code_image(self, file_name): self.driver.save_screenshot(file_name) code_element = self.driver.find_element_by_id("getcode_num") left = code_element.location['x'] top = code_element.location['y'] right = code_element.size['width'] + left height = code_element.size['height'] + top im = Image.open(file_name) img = im.crop((left, top, right, height)) img.save(file_name) time.sleep(1) # 解析圖片獲取驗證碼 def code_online(self, file_name): self.get_code_image(file_name) r = ShowapiRequest("http://route.showapi.com/184-4", "62626", "d61950be50dc4dbd9969f741b8e730f5") r.addBodyPara("typeId", "35") r.addBodyPara("convert_to_jpg", "0") r.addFilePara("image", file_name) # 文件上傳時設置 res = r.post() # print("test:",res.text) time.sleep(1) text = res.json()['showapi_res_body'] # print(text) try: code = text['Result'] return code except Exception as e: print('code_error:',e) return None
二、Unittest介紹及項目實戰中的運用
1、Unittest簡單使用:
import unittest class FirstCase01(unittest.TestCase): @classmethod def setUpClass(cls): print("所有case執行之前的前置") @classmethod def tearDownClass(cls): print("所有case執行之后的后置") def setUp(self): print("這個是case的前置條件") def tearDown(self): print("這個是case的后置調鍵") @unittest.skip("不執行第一條") # 跳過此條case 不執行 def testfirst01(self): print("這個第一條case") def testfirst02(self): print("這是第二條case") def testfirst03(self): print("這是第3條case") if __name__ == '__main__': #unittest.main() suite = unittest.TestSuite() # suite容器 suite.addTest(FirstCase01('testfirst02')) suite.addTest(FirstCase01('testfirst01')) suite.addTest(FirstCase01('testfirst03')) unittest.TextTestRunner().run(suite)
# suite :容器 ,結果是集合類型 suite = unittest.defaultTestLoader.discover(case_path,'unittest_*.py') # 批量選擇性運行case #三個參數:第一個傳入路徑;第二個匹配文件名,成功則將該文件內指定的case存入suite容器中;第三個參數默認為None unittest.TextTestRunner().run(suite) # 執行上述匹配成功的case assertFalse(code_error, "msg") # 用於判斷結果
2、在項目中用HTMLTestRunner輸出漂亮的HTML報告
copy下述HTMLTestRunner代碼,在項目中新建 HTMLTestRunner.py文件,將代碼copy到里面即可使用

""" A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the result at a glance. The simplest way to use this is to invoke its main method. E.g. import unittest import HTMLTestRunner ... define your tests ... if __name__ == '__main__': HTMLTestRunner.main() For more customization options, instantiates a HTMLTestRunner object. HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. # output to a file fp = file('my_report.html', 'wb') runner = HTMLTestRunner.HTMLTestRunner( stream=fp, title='My unit test', description='This demonstrates the report output by HTMLTestRunner.' ) # Use an external stylesheet. # See the Template_mixin class for more customizable options runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' # run the test runner.run(my_test_suite) ------------------------------------------------------------------------ Copyright (c) 2004-2007, Wai Yip Tung All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Wai Yip Tung nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ # URL: http://tungwaiyip.info/software/HTMLTestRunner.html __author__ = "Wai Yip Tung" __version__ = "0.8.2" """ Change History Version 0.8.2 * Show output inline instead of popup window (Viorel Lupu). Version in 0.8.1 * Validated XHTML (Wolfgang Borgert). * Added description of test classes and test cases. Version in 0.8.0 * Define Template_mixin class for customization. * Workaround a IE 6 bug that it does not treat <script> block as CDATA. Version in 0.7.1 * Back port to Python 2.3 (Frank Horowitz). * Fix missing scroll bars in detail log (Podi). """ # TODO: color stderr # TODO: simplify javascript using ,ore than 1 class in the class attribute? import datetime import io import sys import time import unittest from xml.sax import saxutils # ------------------------------------------------------------------------ # The redirectors below are used to capture output during testing. Output # sent to sys.stdout and sys.stderr are automatically captured. However # in some cases sys.stdout is already cached before HTMLTestRunner is # invoked (e.g. calling logging.basicConfig). In order to capture those # output, use the redirectors for the cached stream. # # e.g. # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) # >>> class OutputRedirector(object): """ Wrapper to redirect stdout or stderr """ def __init__(self, fp): self.fp = fp def write(self, s): self.fp.write(s) def writelines(self, lines): self.fp.writelines(lines) def flush(self): self.fp.flush() stdout_redirector = OutputRedirector(sys.stdout) stderr_redirector = OutputRedirector(sys.stderr) # ---------------------------------------------------------------------- # Template class Template_mixin(object): """ Define a HTML template for report customerization and generation. Overall structure of an HTML report HTML +------------------------+ |<html> | | <head> | | | | STYLESHEET | | +----------------+ | | | | | | +----------------+ | | | | </head> | | | | <body> | | | | HEADING | | +----------------+ | | | | | | +----------------+ | | | | REPORT | | +----------------+ | | | | | | +----------------+ | | | | ENDING | | +----------------+ | | | | | | +----------------+ | | | | </body> | |</html> | +------------------------+ """ STATUS = { 0: 'pass', 1: 'fail', 2: 'error', } DEFAULT_TITLE = 'Unit Test Report' DEFAULT_DESCRIPTION = '' # ------------------------------------------------------------------------ # HTML Template HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>%(title)s</title> <meta name="generator" content="%(generator)s"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> %(stylesheet)s </head> <body> <script language="javascript" type="text/javascript"><!-- output_list = Array(); /* level - 0:Summary; 1:Failed; 2:All */ function showCase(level) { trs = document.getElementsByTagName("tr"); for (var i = 0; i < trs.length; i++) { tr = trs[i]; id = tr.id; if (id.substr(0,2) == 'ft') { if (level < 1) { tr.className = 'hiddenRow'; } else { tr.className = ''; } } if (id.substr(0,2) == 'pt') { if (level > 1) { tr.className = ''; } else { tr.className = 'hiddenRow'; } } } } function showClassDetail(cid, count) { var id_list = Array(count); var toHide = 1; for (var i = 0; i < count; i++) { tid0 = 't' + cid.substr(1) + '.' + (i+1); tid = 'f' + tid0; tr = document.getElementById(tid); if (!tr) { tid = 'p' + tid0; tr = document.getElementById(tid); } id_list[i] = tid; if (tr.className) { toHide = 0; } } for (var i = 0; i < count; i++) { tid = id_list[i]; if (toHide) { document.getElementById('div_'+tid).style.display = 'none' document.getElementById(tid).className = 'hiddenRow'; } else { document.getElementById(tid).className = ''; } } } function showTestDetail(div_id){ var details_div = document.getElementById(div_id) var displayState = details_div.style.display // alert(displayState) if (displayState != 'block' ) { displayState = 'block' details_div.style.display = 'block' } else { details_div.style.display = 'none' } } function html_escape(s) { s = s.replace(/&/g,'&'); s = s.replace(/</g,'<'); s = s.replace(/>/g,'>'); return s; } /* obsoleted by detail in <div> function showOutput(id, name) { var w = window.open("", //url name, "resizable,scrollbars,status,width=800,height=450"); d = w.document; d.write("<pre>"); d.write(html_escape(output_list[id])); d.write("\n"); d.write("<a href='javascript:window.close()'>close</a>\n"); d.write("</pre>\n"); d.close(); } */ --></script> %(heading)s %(report)s %(ending)s </body> </html> """ # variables: (title, generator, stylesheet, heading, report, ending) # ------------------------------------------------------------------------ # Stylesheet # # alternatively use a <link> for external style sheet, e.g. # <link rel="stylesheet" href="$url" type="text/css"> STYLESHEET_TMPL = """ <style type="text/css" media="screen"> body { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; } table { font-size: 100%; } pre { } /* -- heading ---------------------------------------------------------------------- */ h1 { font-size: 16pt; color: gray; } .heading { margin-top: 0ex; margin-bottom: 1ex; } .heading .attribute { margin-top: 1ex; margin-bottom: 0; } .heading .description { margin-top: 4ex; margin-bottom: 6ex; } /* -- css div popup ------------------------------------------------------------------------ */ a.popup_link { } a.popup_link:hover { color: red; } .popup_window { display: none; position: relative; left: 0px; top: 0px; /*border: solid #627173 1px; */ padding: 10px; background-color: #E6E6D6; font-family: "Lucida Console", "Courier New", Courier, monospace; text-align: left; font-size: 8pt; width: 500px; } } /* -- report ------------------------------------------------------------------------ */ #show_detail_line { margin-top: 3ex; margin-bottom: 1ex; } #result_table { width: 80%; border-collapse: collapse; border: 1px solid #777; } #header_row { font-weight: bold; color: white; background-color: #777; } #result_table td { border: 1px solid #777; padding: 2px; } #total_row { font-weight: bold; } .passClass { background-color: #6c6; } .failClass { background-color: #c60; } .errorClass { background-color: #c00; } .passCase { color: #6c6; } .failCase { color: #c60; font-weight: bold; } .errorCase { color: #c00; font-weight: bold; } .hiddenRow { display: none; } .testcase { margin-left: 2em; } /* -- ending ---------------------------------------------------------------------- */ #ending { } </style> """ # ------------------------------------------------------------------------ # Heading # HEADING_TMPL = """<div class='heading'> <h1>%(title)s</h1> %(parameters)s <p class='description'>%(description)s</p> </div> """ # variables: (title, parameters, description) HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p> """ # variables: (name, value) # ------------------------------------------------------------------------ # Report # REPORT_TMPL = """ <p id='show_detail_line'>Show <a href='javascript:showCase(0)'>Summary</a> <a href='javascript:showCase(1)'>Failed</a> <a href='javascript:showCase(2)'>All</a> </p> <table id='result_table'> <colgroup> <col align='left' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> </colgroup> <tr id='header_row'> <td>Test Group/Test case</td> <td>Count</td> <td>Pass</td> <td>Fail</td> <td>Error</td> <td>View</td> </tr> %(test_list)s <tr id='total_row'> <td>Total</td> <td>%(count)s</td> <td>%(Pass)s</td> <td>%(fail)s</td> <td>%(error)s</td> <td> </td> </tr> </table> """ # variables: (test_list, count, Pass, fail, error) REPORT_CLASS_TMPL = r""" <tr class='%(style)s'> <td>%(desc)s</td> <td>%(count)s</td> <td>%(Pass)s</td> <td>%(fail)s</td> <td>%(error)s</td> <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td> </tr> """ # variables: (style, desc, count, Pass, fail, error, cid) REPORT_TEST_WITH_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'> <!--css div popup start--> <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" > %(status)s</a> <div id='div_%(tid)s' class="popup_window"> <div style='text-align: right; color:red;cursor:pointer'> <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " > [x]</a> </div> <pre> %(script)s </pre> </div> <!--css div popup end--> </td> </tr> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_NO_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'>%(status)s</td> </tr> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_OUTPUT_TMPL = r""" %(id)s: %(output)s """ # variables: (id, output) # ------------------------------------------------------------------------ # ENDING # ENDING_TMPL = """<div id='ending'> </div>""" # -------------------- The end of the Template class ------------------- TestResult = unittest.TestResult class _TestResult(TestResult): # note: _TestResult is a pure representation of results. # It lacks the output and reporting ability compares to unittest._TextTestResult. def __init__(self, verbosity=1): TestResult.__init__(self) self.stdout0 = None self.stderr0 = None self.success_count = 0 self.failure_count = 0 self.error_count = 0 self.verbosity = verbosity # result is a list of result in 4 tuple # ( # result code (0: success; 1: fail; 2: error), # TestCase object, # Test output (byte string), # stack trace, # ) self.result = [] def startTest(self, test): TestResult.startTest(self, test) # just one buffer for both stdout and stderr self.outputBuffer = io.StringIO() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector def complete_output(self): """ Disconnect output redirection and return buffer. Safe to call multiple times. """ if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None return self.outputBuffer.getvalue() def stopTest(self, test): # Usually one of addSuccess, addError or addFailure would have been called. # But there are some path in unittest that would bypass this. # We must disconnect stdout in stopTest(), which is guaranteed to be called. self.complete_output() def addSuccess(self, test): self.success_count += 1 TestResult.addSuccess(self, test) output = self.complete_output() self.result.append((0, test, output, '')) if self.verbosity > 1: sys.stderr.write('ok ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('.') def addError(self, test, err): self.error_count += 1 TestResult.addError(self, test, err) _, _exc_str = self.errors[-1] output = self.complete_output() self.result.append((2, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('E') def addFailure(self, test, err): self.failure_count += 1 TestResult.addFailure(self, test, err) _, _exc_str = self.failures[-1] output = self.complete_output() self.result.append((1, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('F') class HTMLTestRunner(Template_mixin): """ """ def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None): self.stream = stream self.verbosity = verbosity if title is None: self.title = self.DEFAULT_TITLE else: self.title = title if description is None: self.description = self.DEFAULT_DESCRIPTION else: self.description = description self.startTime = datetime.datetime.now() def run(self, test): "Run the given test case or test suite." result = _TestResult(self.verbosity) test(result) self.stopTime = datetime.datetime.now() self.generateReport(test, result) # print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime) print(sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)) return result def sortResult(self, result_list): # unittest does not seems to run in any particular order. # Here at least we want to group them together by class. rmap = {} classes = [] for n,t,o,e in result_list: cls = t.__class__ if not cls in rmap: rmap[cls] = [] classes.append(cls) rmap[cls].append((n,t,o,e)) r = [(cls, rmap[cls]) for cls in classes] return r def getReportAttributes(self, result): """ Return report attributes as a list of (name, value). Override this to add custom attributes. """ startTime = str(self.startTime)[:19] duration = str(self.stopTime - self.startTime) status = [] if result.success_count: status.append('Pass %s' % result.success_count) if result.failure_count: status.append('Failure %s' % result.failure_count) if result.error_count: status.append('Error %s' % result.error_count ) if status: status = ' '.join(status) else: status = 'none' return [ ('Start Time', startTime), ('Duration', duration), ('Status', status), ] def generateReport(self, test, result): report_attrs = self.getReportAttributes(result) generator = 'HTMLTestRunner %s' % __version__ stylesheet = self._generate_stylesheet() heading = self._generate_heading(report_attrs) report = self._generate_report(result) ending = self._generate_ending() output = self.HTML_TMPL % dict( title = saxutils.escape(self.title), generator = generator, stylesheet = stylesheet, heading = heading, report = report, ending = ending, ) self.stream.write(output.encode('utf8')) def _generate_stylesheet(self): return self.STYLESHEET_TMPL def _generate_heading(self, report_attrs): a_lines = [] for name, value in report_attrs: line = self.HEADING_ATTRIBUTE_TMPL % dict( name = saxutils.escape(name), value = saxutils.escape(value), ) a_lines.append(line) heading = self.HEADING_TMPL % dict( title = saxutils.escape(self.title), parameters = ''.join(a_lines), description = saxutils.escape(self.description), ) return heading def _generate_report(self, result): rows = [] sortedResult = self.sortResult(result.result) for cid, (cls, cls_results) in enumerate(sortedResult): # subtotal for a class np = nf = ne = 0 for n,t,o,e in cls_results: if n == 0: np += 1 elif n == 1: nf += 1 else: ne += 1 # format class description if cls.__module__ == "__main__": name = cls.__name__ else: name = "%s.%s" % (cls.__module__, cls.__name__) doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" desc = doc and '%s: %s' % (name, doc) or name row = self.REPORT_CLASS_TMPL % dict( style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', desc = desc, count = np+nf+ne, Pass = np, fail = nf, error = ne, cid = 'c%s' % (cid+1), ) rows.append(row) for tid, (n,t,o,e) in enumerate(cls_results): self._generate_report_test(rows, cid, tid, n, t, o, e) report = self.REPORT_TMPL % dict( test_list = ''.join(rows), count = str(result.success_count+result.failure_count+result.error_count), Pass = str(result.success_count), fail = str(result.failure_count), error = str(result.error_count), ) return report def _generate_report_test(self, rows, cid, tid, n, t, o, e): # e.g. 'pt1.1', 'ft1.1', etc has_output = bool(o or e) tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) name = t.id().split('.')[-1] doc = t.shortDescription() or "" desc = doc and ('%s: %s' % (name, doc)) or name tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL # o and e should be byte string because they are collected from stdout and stderr? if isinstance(o,str): # TODO: some problem with 'string_escape': it escape \n and mess up formating # uo = unicode(o.encode('string_escape')) # uo = o.decode('latin-1') uo = e else: uo = o if isinstance(e,str): # TODO: some problem with 'string_escape': it escape \n and mess up formating # ue = unicode(e.encode('string_escape')) # ue = e.decode('latin-1') ue = e else: ue = e script = self.REPORT_TEST_OUTPUT_TMPL % dict( id = tid, output = saxutils.escape(str(uo)+ue), ) row = tmpl % dict( tid = tid, Class = (n == 0 and 'hiddenRow' or 'none'), style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), desc = desc, script = script, status = self.STATUS[n], ) rows.append(row) if not has_output: return def _generate_ending(self): return self.ENDING_TMPL ############################################################################## # Facilities for running tests from the command line ############################################################################## # Note: Reuse unittest.TestProgram to launch test. In the future we may # build our own launcher to support more specific command line # parameters like test title, CSS, etc. class TestProgram(unittest.TestProgram): """ A variation of the unittest.TestProgram. Please refer to the base class for command line parameters. """ def runTests(self): # Pick HTMLTestRunner as the default test runner. # base class's testRunner parameter is not useful because it means # we have to instantiate HTMLTestRunner before we know self.verbosity. if self.testRunner is None: self.testRunner = HTMLTestRunner(verbosity=self.verbosity) unittest.TestProgram.runTests(self) main = TestProgram ############################################################################## # Executing this module from the command line ############################################################################## if __name__ == "__main__": main(module=None)
參考:
import unittest from test_mathfunc import TestMathFunc from HTMLTestRunner import HTMLTestRunner if __name__ == '__main__': suite = unittest.TestSuite() suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc)) with open(r'D:\HTMLReport.html', 'wb') as f: runner = HTMLTestRunner(stream=f, title='MathFunc Test Report', description='generated by HTMLTestRunner.', verbosity=2 ) runner.run(suite)

suite = unittest.TestSuite() suite.addTest(FirstCase('test_login_success')) # suite.addTest(FirstCase('test_login_code_error')) suite.addTest(FirstCase('test_login_email_error')) suite.addTest(FirstCase('test_login_username_error')) with open(setting.report_path, 'wb') as f: runner = HTMLTestRunner(stream=f, title="This is first123 report", description="這個是我們第一次測試報告", verbosity=2 ) runner.run(suite)
* unittest中,執行case時,如果執行失敗,錯誤信息會被保存到_outtime中,我們可通過一些技巧對這些錯誤信息進行操作,比如:case執行完,如果出錯,可以通過tearDown方法 將錯誤信息頁面截圖,保存到指定路徑,方便檢查出錯原因 ↓
def tearDown(self): for method_name, error in self._outcome.errors: # case如果執行失敗,錯誤會保存到_outcome.errors 中 if error: # 將錯誤信息截圖,保存到指定路徑 case_name = self._testMethodName # case名,即定義好的方法名 report_error_name = case_name + '.png' report_error_path = os.path.join(setting.base_dir,'report', report_error_name) print("report_error:", report_error_name) self.driver.save_screenshot(report_error_path)
使用unittest框架 + HTMLTestRunner 結合,優化 first_case.py 代碼:
from selenium import webdriver from setting import setting from business.register_business import RegisterBusiness import unittest import os from util.HTMLTestRunner import HTMLTestRunner class FirstCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = webdriver.Chrome() cls.driver.get('http://www.5itest.cn/register') cls.driver.maximize_window() cls.file_name = setting.code_path cls.login = RegisterBusiness(cls.driver) def setUp(self): self.driver.refresh() # self.login = RegisterBusiness(self.driver) def tearDown(self): for method_name, error in self._outcome.errors: # case如果執行失敗,錯誤會保存到_outcome.errors 中 if error: case_name = self._testMethodName # case名,即定義好的方法名 report_error_name = case_name + '.png' report_error_path = os.path.join(setting.base_dir,'report', report_error_name) print("report_error:", report_error_name) self.driver.save_screenshot(report_error_path) @classmethod def tearDownClass(cls): cls.driver.close() def test_login_success(self): login_success = self.login.register_succes() return self.assertFalse(login_success, "注冊成功,case調試失敗!") def test_login_email_error(self): email_error = self.login.login_email_error('1586457@qq.com','vvv','221111',self.file_name) return self.assertFalse(email_error, "郵箱格式輸入正確,此條case執行失敗!") # 判斷email_error是否為False,如是表示測試通過,如不是表示測試失敗,顯示參數二的信息 # if email_error == True: # print("Error: email沒有錯誤提示,此條case執行失敗!") def test_login_username_error(self): # self.driver.refresh() username_error = self.login.login_name_error('qq.com','bbbaac','111111',self.file_name) return self.assertFalse(username_error, "用戶名格式輸入正確,此條case執行失敗!") def test_login_password_error(self): # self.driver.refresh() password_error = self.login.login_password_error('3333666@qq.com','ccc','1114441',self.file_name) return self.assertFalse(password_error, "密碼格式輸入正確,此條case執行失敗!") def test_login_code_error(self): # self.driver.refresh() code_error = self.login.login_code_error('44465745444@qq.com','ddfghdddd','1111',self.file_name) return self.assertFalse(code_error, "驗證碼格式輸入正確,此條case執行失敗!") # def main_run(self): # # self.test_login_email_error() # self.test_login_username_error() # self.test_login_password_error() # self.test_login_code_error() # self.driver.close() if __name__ == '__main__': # file_name = setting.code_path # url = 'http://www.5itest.cn/register' # first_case = FirstCase(file_name,url) # first_case.main_run() suite = unittest.TestSuite() suite.addTest(FirstCase('test_login_email_error')) suite.addTest(FirstCase('test_login_username_error')) suite.addTest(FirstCase('test_login_password_error')) suite.addTest(FirstCase('test_login_code_error')) suite.addTest(FirstCase('test_login_success')) with open(setting.report_path, 'wb') as f: runner = HTMLTestRunner(stream=f, title="This is first123 report", description="這個是我們第一次測試報告", verbosity=2 ) runner.run(suite)
二、數據驅動
使用數據驅動,可以去除冗余。如同一方法傳入不同參數這種,可以使用數據驅動程序,根據需求傳入不同參數,驅動同一程序執行。
1、ddt安裝:pip install ddt
ddt簡單使用:
import ddt import unittest @ddt.ddt # DataTest類加ddt裝飾器 class DataTest(unittest.TestCase): def setUp(self): print("這個是setup") def tearDown(self): print("這個是teardown") @ddt.data( # ddt數據 [1,2,3,4], [3,4], [5,6] ) @ddt.unpack #傳遞的是復雜的數據結構時使用。比如使用元組或者列表,不是復雜的數據不需要用到unpack def test_add(self,a,b): print(a+b) if __name__ == '__main__': unittest.main()
使用方法:
# 使用方法 dd.ddt: # 裝飾類,也就是繼承自TestCase的類。 ddt.data: # 裝飾測試方法。參數是一系列的值。 ddt.file_data: # 裝飾測試方法。參數是文件名。文件可以是json 或者 yaml類型。 # 注意,如果文件以”.yml”或者”.yaml”結尾,ddt會作為yaml類型處理,其他所有文件都會作為json文件處理。 # 如果文件中是列表,每個列表的值會作為測試用例參數,同時作為測試用例方法名后綴顯示。 # 如果文件中是字典,字典的key會作為測試用例方法的后綴顯示,字典的值會作為測試用例參數。 ddt.unpack: # 傳遞的是復雜的數據結構時使用。比如使用元組或者列表,添加unpack之后,ddt會自動把元組或者列表對應到多個參數上。字典也可以這樣處理
使用json文件:
新建文件 test_data_list.json:
[ "Hello", "Goodbye" ]
新建文件 test_data_dict.json:
{ "unsorted_list": [ 10, 12, 15 ], "sorted_list": [ 15, 12, 50 ] }
數據驅動測試腳本ddt_test.py:
import unittest from ddt import ddt, file_data from ddt_demo.mycode import has_three_elements,is_a_greeting @ddt class FooTestCase(unittest.TestCase): @file_data('test_data_dict.json') def test_file_data_json_dict(self, value): self.assertTrue(has_three_elements(value)) @file_data('test_data_list.json') def test_file_data_json_list(self, value): self.assertTrue(is_a_greeting(value)) if __name__=='__main__': unittest.main(verbosity=2)
2、以文件的形式實現數據驅動( excel表 )
1)xlrd安裝:
pip install xlrd
2)通過xlrd 獲取excel表中數據行數,將每行數據以列表形式添加到一個大列表中
import xlrd from setting.setting import excel_path class Excel_Opertion(object): """excel表數據相關操作""" def __init__(self,ex_path=None,index=None): if ex_path == None: self.excel_path = excel_path # 默認excel文件路徑 else: self.excel_path = ex_path if index == None: index = 0 self.data = xlrd.open_workbook(self.excel_path) self.table = self.data.sheets()[index] # sheets第一頁數據 # 獲取excel數據,按照每行一個list,添加到一個大的list里面 def get_data(self): result = [] rows = self.get_lines() if rows !=None: for i in range(1,rows): row = self.table.row_values(i) # print(row) # ['test001@qq.com', 'Mushishi001', '111111', 'code', 'user_email_error', '請輸入有效的電子郵件地址'] result.append(row) # [['test001@qq.com', 'Mushishi001', '111111', 'code', 'user_email_error', '請輸入有效的電子郵件地址'], ['test002.com', 'Mushishi002', '111112', 'code', 'user_email_error', '請輸入有效的電子郵件地址']] return result return None # 獲取excel行數 def get_lines(self): rows = self.table.nrows # 獲取行數 if rows > 1: return rows return None
3)數據驅動case
從excel表獲取數據,通過ddt模塊實現數據驅動模式,數據 → 驅動 → 程序,循環調用 test_register_case() 方法,直至excel表數據執行完
mport ddt import unittest import os from selenium import webdriver from setting import setting from business.register_business import RegisterBusiness from util.excel_operation import Excel_Opertion from util.HTMLTestRunner import HTMLTestRunner ex_opr = Excel_Opertion() # 實例化 ex_data = ex_opr.get_data() # 獲取excel表中每行數據 @ddt.ddt class FirstDdtCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = webdriver.Chrome() cls.driver.get('http://www.5itest.cn/register') cls.driver.maximize_window() cls.file_name = setting.code_path cls.login = RegisterBusiness(cls.driver) def setUp(self): self.driver.refresh() # self.login = RegisterBusiness(self.driver) def tearDown(self): for method_name, error in self._outcome.errors: # case如果執行失敗,錯誤會保存到_outcome.errors 中 if error: # case_name = self._testMethodName # case名,即定義好的方法名 report_error_name = self.assertCode + '.png' report_error_path = os.path.join(setting.base_dir, 'report', report_error_name) self.driver.save_screenshot(report_error_path) @classmethod def tearDownClass(cls): cls.driver.close() @ddt.data(*ex_data) def test_register_case(self,ex_data): # ex_data:[[],[],..] 列表套列表 """數據驅動模式,會按ex_data列表數據,一行一行循環執行,直至列表數據執行完畢""" email, username, password, self.assertCode, assertText = ex_data # 將ex_data每個子列表的數據按順序賦值。驗證碼需要提供路徑,單獨給 # 郵箱、用戶名、密碼、驗證碼、錯誤信息定位元素、錯誤提示信息 register_error = self.login.register_function(email, username, password, self.file_name,self.assertCode, assertText) self.assertFalse(register_error,"測試失敗:{}".format(self.assertCode))
test_register_case方法:

# 數據驅動,只執行此條代碼 # 郵箱、用戶名、密碼、驗證碼、錯誤信息定位元素、錯誤提示信息 def register_function(self,email,username,password,file_name,assertCode,assertText): self.user_base(email,username,password,file_name) if self.register_h.get_user_text(assertCode,assertText) == None: return True else: return False