前言
明天就放假了,4天小長假,是不是很開心!也許很多人要回老家幫家裏種地,幹農活。其實能陪陪家裏人,幫忙乾點農活還是挺開心的,希望大家有個愉快的假期!廢話不多說哈,今天再來說說pytest吧,經過幾周的時間學習,有收穫也有疑惑,總之最後還是搞個小項目出來證明自己的努力不沒有白費
環境準備
序號 | 庫/插件/工具 | 安裝命令 |
1 | 確保您已經安裝了python3.x | |
2 | 配置python3+pycharm+selenium2開發環境 | |
3 | 安裝pytest庫 | pip install pytest |
4 | 安裝pytest -html 報告插件 | pip install pytest-html |
5 | 安裝pypiwin32庫(用來模擬按鍵) | pip install pypiwin32 |
6 | 安裝openpyxl解析excel文件庫 | pip install openpyxl |
7 | 安裝yagmail發送報告庫 | pip install yagmail |
8 | 確保已配置火狐或谷歌瀏覽器及對應驅動 | |
9 | 確保已經正確配置好發送郵件的郵箱 |
項目簡介
測試地址
測試範圍
1.126電子郵箱登錄功能測試-驗證正確帳號密碼登錄成功-驗證錯誤用戶名密碼登錄失敗(有很多情況,用例裏面做了充分的校驗)
2.126電子郵箱添加聯繫人功能測試-驗證正確填寫必填項數據添加聯繫人成功-驗證缺省必填項數據添加聯繫人失敗-驗證必填項字段數據格式錯誤添加聯繫人失敗
3.126電子郵箱發送郵件功能測試-驗證普通郵件發送成功-驗證帶附件郵件發送成功
項目設計
1.python編程語言設計測試腳本
2.webdriver驅動瀏覽器並操作頁面元素
3.二次封裝webdriver Api 操作方法
4.採用PageObject設計模式,設計測試業務流程
5.通過UI對象庫存儲頁面操作元素
6.通過數據文件存儲數據,讀取數據,參數化測試用例並驅動測試執行
7.通過第三方插件pytest-html生成測試報告
8.通過yagmail第三方庫,編寫發送報告接口,測試工作完成後自動發送測試報告
代碼分析
目錄結構
1 PytestAutoTestFrameWork 2 |—|config 3 |——|__init__.py 4 |——|conf.py 5 |——|config.ini 6 |—|data 7 |——|__init__.py 8 |——|tcData.xlsx 9 |—Page 10 |——|PageObject.py 11 |———|__init__.py 12 |———|ContactPage.py 13 |———|HomePage.py 14 |———|LoginPage.py 15 |———|SendMailPage.py 16 |——|__init__.py 17 |——|BasePage.py 18 |—|report 19 |—|TestCases 20 |——|__init__.py 21 |——|conftest.py 22 |——|test_confactCase.py 23 |——|test_loginCase.py 24 |——|test_sendMailCase.py 25 |—|util 26 |——|__init__.py 27 |——|clipboard.py 28 |——|keyboard.py 29 |——|parseConFile.py 30 |——|parseExcelFile.py 31 |——|sendMailForReport.py 32 |—|conftest.py 33 |—|pytest.ini 34 |—|RunTestCase.py
代碼實現
通過126郵箱測試範圍分析,我們需要通過設計剪切板,模擬鍵盤完成附件上傳操作,因此我們首先來編寫這兩個方法
1 """ 2 ------------------------------------ 3 @Time : 2019/4/15 12:04 4 @Auth : linux超 5 @File : clipboard.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 import win32con 11 import win32clipboard as WC 12 13 14 class ClipBoard(object): 15 '''設置剪切板內容和獲取剪切板內容''' 16 17 @staticmethod 18 def getText(): 19 '''獲取剪切板的內容''' 20 WC.OpenClipboard() 21 value = WC.GetClipboardData(win32con.CF_TEXT) 22 WC.CloseClipboard() 23 return value 24 25 @staticmethod 26 def setText(value): 27 '''設置剪切板的內容''' 28 WC.OpenClipboard() 29 WC.EmptyClipboard() 30 WC.SetClipboardData(win32con.CF_UNICODETEXT, value) 31 WC.CloseClipboard() 32 33 34 if __name__ == '__main__': 35 from selenium import webdriver 36 37 value = 'python' 38 driver = webdriver.Firefox() 39 driver.get('http://www.baidu.com') 40 query = driver.find_element_by_id('kw') 41 ClipBoard.setText(value) 42 clValue = ClipBoard.getText() 43 query.send_keys(clValue.decode('utf-8'))
1 """ 2 ------------------------------------ 3 @Time : 2019/4/15 12:05 4 @Auth : linux超 5 @File : keyboard.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 11 # 模擬按鍵 12 import win32api 13 import win32con 14 import time 15 16 17 class KeyBoard(object): 18 """模擬按鍵""" 19 # 鍵盤碼 20 vk_code = { 21 'enter' : 0x0D, 22 'tab' : 0x09, 23 'ctrl' : 0x11, 24 'v' : 0x56, 25 'a' : 0x41, 26 'x' : 0x58 27 } 28 29 @staticmethod 30 def keyDown(key_name): 31 """按下鍵""" 32 key_name = key_name.lower() 33 try: 34 win32api.keybd_event(KeyBoard.vk_code[key_name], 0, 0, 0) 35 except Exception as e: 36 print('未按下enter鍵') 37 print(e) 38 39 @staticmethod 40 def keyUp(key_name): 41 """擡起鍵""" 42 key_name = key_name.lower() 43 win32api.keybd_event(KeyBoard.vk_code[key_name], 0, win32con.KEYEVENTF_KEYUP, 0) 44 45 @staticmethod 46 def oneKey(key): 47 """模擬單個按鍵""" 48 key = key.lower() 49 KeyBoard.keyDown(key) 50 time.sleep(2) 51 KeyBoard.keyUp(key) 52 53 @staticmethod 54 def twoKeys(key1, key2): 55 """模擬組合按鍵""" 56 key1 = key1.lower() 57 key2 = key2.lower() 58 KeyBoard.keyDown(key1) 59 KeyBoard.keyDown(key2) 60 KeyBoard.keyUp(key1) 61 KeyBoard.keyUp(key2) 62 63 64 if __name__ == '__main__': 65 from selenium import webdriver 66 driver = webdriver.Firefox() 67 driver.get('http://www.baidu.com') 68 driver.find_element_by_id('kw').send_keys('python') 69 KeyBoard.twoKeys('ctrl', 'a') 70 KeyBoard.twoKeys('ctrl', 'x')
通過測試項目設計,我們需要把測試數據存放在Excel文件中,把頁面操作元素存在UI對象庫中也就是一個配置文件,那麼我們需要對Excel 和 ini文件解析,因此我們開始編寫這兩個方法,設計UI對象庫和測試數據文件
1 """ 2 ------------------------------------ 3 @Time : 2019/4/22 16:12 4 @Auth : linux超 5 @File : parseExcelFile.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 from openpyxl import load_workbook 11 from config.conf import excelPath 12 13 14 class ParseExcel(object): 15 16 def __init__(self): 17 self.wk = load_workbook(excelPath) 18 self.excelFile = excelPath 19 20 def getSheetByName(self, sheetName): 21 """獲取sheet對象""" 22 sheet = self.wk[sheetName] 23 return sheet 24 25 def getRowNum(self, sheet): 26 """獲取有效數據的最大行號""" 27 return sheet.max_row 28 29 def getColsNum(self, sheet): 30 """獲取有效數據的最大列號""" 31 return sheet.max_column 32 33 def getRowValues(self, sheet, rowNum): 34 """獲取某一行的數據""" 35 maxColsNum = self.getColsNum(sheet) 36 rowValues = [] 37 for colsNum in range(1, maxColsNum + 1): 38 value = sheet.cell(rowNum, colsNum).value 39 if value is None: 40 value = '' 41 rowValues.append(value) 42 return tuple(rowValues) 43 44 def getColumnValues(self, sheet, columnNum): 45 """獲取某一列的數據""" 46 maxRowNum = self.getRowNum(sheet) 47 columnValues = [] 48 for rowNum in range(2, maxRowNum + 1): 49 value = sheet.cell(rowNum, columnNum).value 50 if value is None: 51 value = '' 52 columnValues.append(value) 53 return tuple(columnValues) 54 55 def getValueOfCell(self, sheet, rowNum, columnNum): 56 """獲取某一個單元格的數據""" 57 value = sheet.cell(rowNum, columnNum).value 58 if value is None: 59 value = '' 60 return value 61 62 def getAllValuesOfSheet(self, sheet): 63 """獲取某一個sheet頁的所有測試數據,返回一個元祖組成的列表""" 64 maxRowNum = self.getRowNum(sheet) 65 columnNum = self.getColsNum(sheet) 66 allValues = [] 67 for row in range(2, maxRowNum + 1): 68 rowValues = [] 69 for column in range(1, columnNum + 1): 70 value = sheet.cell(row, column).value 71 if value is None: 72 value = '' 73 rowValues.append(value) 74 allValues.append(tuple(rowValues)) 75 return allValues 76 77 78 if __name__ == '__main__': 79 # excel = ParseExcel() 80 # sheet = excel.getSheetByName('login') 81 # print('行號:', excel.getRowNum(sheet)) 82 # print('列號:', excel.getColsNum(sheet)) 83 # 84 # rowvalues = excel.getRowValues(sheet, 1) 85 # columnvalues = excel.getColumnValues(sheet, 2) 86 # valueofcell = excel.getValueOfCell(sheet, 1, 2) 87 # allvalues = excel.getAllValuesOfSheet(sheet) 88 # 89 # print('第{}行數據{}'.format(1, rowvalues)) 90 # print('第{}列數據{}'.format(2, columnvalues)) 91 # print('{}{}單元格的內容{}'.format(1, 2, valueofcell)) 92 # print('login{}'.format(allvalues)) 93 94 excel = ParseExcel() 95 sheet = excel.getSheetByName('mail') 96 print('行號:', excel.getRowNum(sheet)) 97 print('列號:', excel.getColsNum(sheet)) 98 99 allvalues = excel.getAllValuesOfSheet(sheet) 100 101 print('sendmail{}'.format(allvalues))
1 """ 2 ------------------------------------ 3 @Time : 2019/4/18 10:54 4 @Auth : linux超 5 @File : parseConFile.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 import configparser 11 from config.conf import configDir 12 13 14 class ParseConFile(object): 15 16 def __init__(self): 17 self.file = configDir 18 self.conf = configparser.ConfigParser() 19 self.conf.read(self.file, encoding='utf-8') 20 21 def getAllSections(self): 22 """獲取所有的section,返回一個列表""" 23 return self.conf.sections() 24 25 def getAllOptions(self, section): 26 """獲取指定section下所有的option, 返回列表""" 27 return self.conf.options(section) 28 29 def getLocatorsOrAccount(self, section, option): 30 """獲取指定section, 指定option對應的數據, 返回元祖和字符串""" 31 try: 32 locator = self.conf.get(section, option) 33 if ('->' in locator): 34 locator = tuple(locator.split('->')) 35 return locator 36 except configparser.NoOptionError as e: 37 print('error:', e) 38 return 'error: No option "{}" in section: "{}"'.format(option, section) 39 40 def getOptionValue(self, section): 41 """獲取指定section下所有的option和對應的數據,返回字典""" 42 value = dict(self.conf.items(section)) 43 return value 44 45 46 if __name__ == '__main__': 47 cf = ParseConFile() 48 print(cf.getAllSections()) 49 print(cf.getAllOptions('126LoginAccount')) 50 print(cf.getLocatorsOrAccount('126LoginAccount', 'username')) 51 print(cf.getOptionValue('126LoginAccount'))
1 [126LoginAccount];126郵箱正確的登錄賬號和密碼;運行用例時請更換正確的用戶名和密碼 2 username=linuxxiaochao 3 password=xiaochao11520 4 [HomePageElements];126郵箱首頁菜單欄元素 5 homePage=id->_mail_tabitem_0_3text 6 mailList=id->_mail_tabitem_1_4text 7 applicationCenter=id->_mail_tabitem_2_5text 8 inBox=id->_mail_tabitem_3_6text 9 [LoginPageElements];126郵箱登錄頁面的元素 10 frame=xpath->//div[@id="loginDiv"]/iframe 11 username=xpath->//input[@name="email"] 12 password=xpath->//input[@name="password"] 13 loginBtn=xpath->//a[@id="dologin"] 14 ferrorHead=xpath->//div[@class="ferrorhead"] 15 [ContactPageElements];126郵箱添加聯繫人頁面元素 16 new_contact=xpath->//span[text()="新建聯繫人"] 17 name=id->input_N 18 mail=xpath->//div[@id="iaddress_MAIL_wrap"]//input[@class="nui-ipt-input"] 19 star=xpath->//span[@class="nui-chk-text"]/preceding-sibling::span/b 20 phone=xpath->//div[@id='iaddress_TEL_wrap']//input[@class='nui-ipt-input'] 21 comment=id->input_DETAIL 22 commit=xpath->//span[text()='確 定'] 23 tooltip=xpath->//span[text()='請正確填寫郵件地址。'] 24 [SendMailPageElements];126郵箱發送郵件頁面元素 25 writeMail=xpath->//div[@id='dvNavContainer']//span[text()='寫 信'] 26 addressee=xpath->//input[@aria-label='收件人地址輸入框,請輸入郵件地址,多人時地址請以分號隔開'] 27 subject=xpath->//input[contains(@id, '_subjectInput')] 28 iframe=xpath->//iframe[@class="APP-editor-iframe"] 29 text=xpath->/html/body 30 sendBtn=xpath->//header//span[text()='發送'] 31 expect=xpath->//h1[contains(@id,'_succInfo')] 32 uploadAttachment=xpath->//div[@title="點擊添加附件"] 33 delete=xpath->//a[text()='刪除']
新建excel文件,分3個sheet,分別爲:login,contact,mail #每個sheet中數據可自行填寫,驅動測試用例執行不同的數據進行測試
login
contact
數據,UI對象庫,解析方法都已經有了,接下來通過PageObject模式設計編寫每個頁面的操作及封裝126郵箱的功能,以便後續設計用例調用
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 8:45 4 @Auth : linux超 5 @File : BasePage.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 import time 11 from selenium.webdriver.support import expected_conditions as EC 12 from selenium.webdriver.support.wait import WebDriverWait as wd 13 from selenium.webdriver.common.by import By 14 from selenium.common.exceptions import NoSuchWindowException, TimeoutException, \ 15 NoAlertPresentException, NoSuchFrameException 16 from selenium import webdriver 17 18 from util.clipboard import ClipBoard 19 from util.keyboard import KeyBoard 20 from util.parseConFile import ParseConFile 21 from util.parseExcelFile import ParseExcel 22 23 24 class BasePage(object): 25 """ 26 結合顯示等待封裝一些selenium 內置方法 27 """ 28 cf = ParseConFile() 29 excel = ParseExcel() 30 31 def __init__(self, driver, outTime=30): 32 self.byDic = { 33 'id': By.ID, 34 'name': By.NAME, 35 'class_name': By.CLASS_NAME, 36 'xpath': By.XPATH, 37 'link_text': By.LINK_TEXT 38 } 39 self.driver = driver 40 self.outTime = outTime 41 42 def findElement(self, by, locator): 43 """ 44 find alone element 45 :param by: eg: id, name, xpath, css..... 46 :param locator: id, name, xpath for str 47 :return: element object 48 """ 49 try: 50 print('[Info:Starting find the element "{}" by "{}"!]'.format(locator, by)) 51 element = wd(self.driver, self.outTime).until(lambda x : x.find_element(by, locator)) 52 except TimeoutException as t: 53 print('error: found "{}" timeout!'.format(locator), t) 54 except NoSuchWindowException as e: 55 print('error: no such "{}"'.format(locator), e) 56 except Exception as e: 57 raise e 58 else: 59 # print('[Info:Had found the element "{}" by "{}"!]'.format(locator, by)) 60 return element 61 62 def findElements(self, by, locator): 63 """ 64 find group elements 65 :param by: eg: id, name, xpath, css..... 66 :param locator: eg: id, name, xpath for str 67 :return: elements object 68 """ 69 try: 70 print('[Info:start find the elements "{}" by "{}"!]'.format(locator, by)) 71 elements = wd(self.driver, self.outTime).until(lambda x : x.find_element(by, locator)) 72 except TimeoutException as t: 73 print(t) 74 except NoSuchWindowException as e: 75 print(e) 76 except Exception as e: 77 raise e 78 else: 79 # print('[Info:Had found the elements "{}" by "{}"!]'.format(locator, by)) 80 return elements 81 82 def isElementExsit(self, by, locator): 83 """ 84 assert element if exist 85 :param by: eg: id, name, xpath, css..... 86 :param locator: eg: id, name, xpath for str 87 :return: if element return True else return false 88 """ 89 if by.lower() in self.byDic: 90 try: 91 wd(self.driver, self.outTime).\ 92 until(EC.visibility_of_element_located((self.byDic[by], locator))) 93 except TimeoutException: 94 print('Error: element "{}" time out!'.format(locator)) 95 return False 96 except NoSuchWindowException: 97 print('Error: element "{}" not exsit!'.format(locator)) 98 return False 99 return True 100 else: 101 print('the "{}" error!'.format(by)) 102 103 def isClick(self, by, locator): 104 """判斷是否可點擊,返回元素對象""" 105 if by.lower() in self.byDic: 106 try: 107 element = wd(self.driver, self.outTime).\ 108 until(EC.element_to_be_clickable((self.byDic[by], locator))) 109 except Exception: 110 return False 111 return element 112 else: 113 print('the "{}" error!'.format(by)) 114 115 def isAlertAndSwitchToIt(self): 116 """ 117 assert alert if exsit 118 :return: alert obj 119 """ 120 try: 121 re = wd(self.driver, self.outTime).until(EC.alert_is_present()) 122 except NoAlertPresentException: 123 return False 124 except Exception: 125 return False 126 return re 127 128 def switchToFrame(self, by, locator): 129 """判斷frame是否存在,存在就跳到frame""" 130 print('info:switching to iframe "{}"'.format(locator)) 131 if by.lower() in self.byDic: 132 try: 133 wd(self.driver, self.outTime).\ 134 until(EC.frame_to_be_available_and_switch_to_it((self.byDic[by], locator))) 135 except TimeoutException as t: 136 print('error: found "{}" timeout!'.format(locator), t) 137 except NoSuchFrameException as e: 138 print('error: no such "{}"'.format(locator), e) 139 except Exception as e: 140 raise e 141 else: 142 print('the "{}" error!'.format(by)) 143 144 def switchToDefaultFrame(self): 145 """返回默認的frame""" 146 print('info:switch back to default iframe') 147 try: 148 self.driver.switch_to.default_content() 149 except Exception as e: 150 print(e) 151 152 def getAlertText(self): 153 """獲取alert的提示信息""" 154 if self.isAlertAndSwitchToIt(): 155 alert = self.isAlertAndSwitchToIt() 156 return alert.text 157 else: 158 return None 159 160 def getElementText(self, by, locator, name=None): 161 """獲取某一個元素的text信息""" 162 try: 163 element = self.findElement(by, locator) 164 if name: 165 return element.get_attribute(name) 166 else: 167 return element.text 168 except: 169 print('get "{}" text failed return None'.format(locator)) 170 return None 171 172 def loadUrl(self, url): 173 """加載url""" 174 print('info: string upload url "{}"'.format(url)) 175 self.driver.get(url) 176 177 def getSource(self): 178 """獲取頁面源碼""" 179 return self.driver.page_source 180 181 def sendKeys(self, by, locator, value=''): 182 """寫數據""" 183 print('info:input "{}"'.format(value)) 184 try: 185 element = self.findElement(by, locator) 186 element.send_keys(value) 187 except AttributeError as e: 188 print(e) 189 190 def clear(self, by, locator): 191 """清理數據""" 192 print('info:clearing value') 193 try: 194 element = self.findElement(by, locator) 195 element.clear() 196 except AttributeError as e: 197 print(e) 198 199 def click(self, by, locator): 200 """點擊某個元素""" 201 print('info:click "{}"'.format(locator)) 202 element = self.isClick(by, locator) 203 if element: 204 element.click() 205 else: 206 print('the "{}" unclickable!') 207 208 def sleep(self, num=0): 209 """強制等待""" 210 print('info:sleep "{}" minutes'.format(num)) 211 time.sleep(num) 212 213 def ctrlV(self, value): 214 """ctrl + V 粘貼""" 215 print('info:pasting "{}"'.format(value)) 216 ClipBoard.setText(value) 217 self.sleep(3) 218 KeyBoard.twoKeys('ctrl', 'v') 219 220 def enterKey(self): 221 """enter 回車鍵""" 222 print('info:keydown enter') 223 KeyBoard.oneKey('enter') 224 225 def waitElementtobelocated(self, by, locator): 226 """顯示等待某個元素出現,且可見""" 227 print('info:waiting "{}" to be located'.format(locator)) 228 try: 229 wd(self.driver, self.outTime).until(EC.visibility_of_element_located((self.byDic[by], locator))) 230 except TimeoutException as t: 231 print('error: found "{}" timeout!'.format(locator), t) 232 except NoSuchWindowException as e: 233 print('error: no such "{}"'.format(locator), e) 234 except Exception as e: 235 raise e 236 237 def assertValueInSource(self, value): 238 """斷言某個關鍵字是否存在頁面源碼中""" 239 print('info:assert "{}" in page source'.format(value)) 240 source = self.getSource() 241 assert value in source, '關鍵字"{}"不存在源碼中!'.format(value) 242 243 def assertStringContainsValue(self, String, value): 244 """斷言某段字符串包含另一個字符串""" 245 print('info:assert "{}" contains "{}"'.format(String, value)) 246 assert value in String, '"{}"不包含"{}"!'.format(String, value) 247 248 249 @staticmethod 250 def getSheet(sheetName): 251 """獲取某個sheet頁的對象""" 252 sheet = BasePage.excel.getSheetByName(sheetName) 253 return sheet 254 255 256 if __name__ == "__main__": 257 driver = webdriver.Firefox() 258 frame = ('xpath', '//div[@id="loginDiv"]/ifram') 259 wait = BasePage(driver) 260 driver.get('https://mail.126.com/') 261 wait.switchToFrame(*frame) 262 username = wait.findElement('xpath', '//input[@name="email"]') 263 username.send_keys('賬號') 264 if wait.isElementExsit('xpath', '//input[@name="password"]'): 265 wait.findElement('xpath', '//input[@name="password"]').send_keys('xiaochao11520') 266 wait.click('xpath', '//a[@id="dologin"]')
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 12:28 4 @Auth : linux超 5 @File : HomePage.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 from Page.BasePage import BasePage 11 12 13 class HomePage(BasePage): 14 # 配置文件讀取元素 15 homePage = BasePage.cf.getLocatorsOrAccount('HomePageElements', 'homePage') 16 mailList = BasePage.cf.getLocatorsOrAccount('HomePageElements', 'mailList') 17 applicationCenter = BasePage.cf.getLocatorsOrAccount('HomePageElements', 'applicationCenter') 18 inBox = BasePage.cf.getLocatorsOrAccount('HomePageElements', 'inBox') 19 '''首頁菜單選項''' 20 def selectMenu(self, Menu='mailList'): 21 """郵箱首頁選擇菜單""" 22 if Menu == 'mailList': 23 self.click(*HomePage.mailList) 24 elif Menu == 'homePage': 25 self.click(*HomePage.homePage) 26 elif Menu == 'applicationCenter': 27 self.click(*HomePage.applicationCenter) 28 elif Menu == 'inBox': 29 self.click(*HomePage.inBox) 30 else: 31 raise ValueError(''' 32 菜單選擇錯誤! 33 homePage->首頁 34 mailList->通訊錄 35 applicationCenter->應用中心 36 inBox->收件箱''') 37 38 if __name__=='__main__': 39 from selenium import webdriver 40 from Page.PageObject.LoginPage import LoginPage 41 driver = webdriver.Firefox() 42 login = LoginPage(driver) 43 login.login('賬號', 'xiaochao11520') 44 45 home = HomePage(driver) 46 home.selectMenu()
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 12:28 4 @Auth : linux超 5 @File : LoginPage.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 from Page.BasePage import BasePage 11 12 13 class LoginPage(BasePage): 14 15 # 配置文件讀取元素 16 frame = BasePage.cf.getLocatorsOrAccount('LoginPageElements', 'frame') 17 username = BasePage.cf.getLocatorsOrAccount('LoginPageElements', 'username') 18 password = BasePage.cf.getLocatorsOrAccount('LoginPageElements', 'password') 19 loginBtn = BasePage.cf.getLocatorsOrAccount('LoginPageElements', 'loginBtn') 20 ferrorHead = BasePage.cf.getLocatorsOrAccount('LoginPageElements', 'ferrorHead') # 登錄失敗提示 21 22 def login(self, userName, passWord): 23 '''登錄''' 24 print('-------staring login-------') 25 self.loadUrl('https://mail.126.com') 26 self.switchToFrame(*LoginPage.frame) 27 self.clear(*LoginPage.username) 28 self.sendKeys(*LoginPage.username, userName) 29 self.clear(*LoginPage.password) 30 self.sendKeys(*LoginPage.password, passWord) 31 self.click(*LoginPage.loginBtn) 32 self.switchToDefaultFrame() 33 print('---------end login---------') 34 35 # add at 2019/04/19 36 def assertTextEqString(self, expected, name = None): 37 '''斷言提示信息是否與期望的值相等''' 38 self.switchToFrame(*LoginPage.frame) 39 text = self.getElementText(*LoginPage.ferrorHead, name) 40 self.switchToDefaultFrame() 41 print('info: assert "{}" == "{}"'.format(text, expected)) 42 assert text == expected, '{} != {}'.format(text, expected) 43 44 if __name__=="__main__": 45 from selenium import webdriver 46 driver = webdriver.Firefox() 47 login = LoginPage(driver, 30) 48 login.login('lin', '') 49 login.assertTextEqString('請輸入密碼')
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 12:29 4 @Auth : linux超 5 @File : ContactPage.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 from Page.BasePage import BasePage 11 12 13 class ContactPage(BasePage): 14 # 配置文件讀取元素 15 new_contact = BasePage.cf.getLocatorsOrAccount('ContactPageElements', 'new_contact') 16 name = BasePage.cf.getLocatorsOrAccount('ContactPageElements', 'name') 17 mail = BasePage.cf.getLocatorsOrAccount('ContactPageElements', 'mail') 18 star = BasePage.cf.getLocatorsOrAccount('ContactPageElements', 'star') 19 phone = BasePage.cf.getLocatorsOrAccount('ContactPageElements', 'phone') 20 comment = BasePage.cf.getLocatorsOrAccount('ContactPageElements', 'comment') 21 commit = BasePage.cf.getLocatorsOrAccount('ContactPageElements', 'commit') 22 errortip = BasePage.cf.getLocatorsOrAccount('ContactPageElements', 'tooltip') # 錯誤提示 23 24 def newContact(self, Name, Mail, Star, Phone, Comment): 25 """添加聯繫人""" 26 print('--------string add contact--------') 27 self.click(*ContactPage.new_contact) 28 self.sendKeys(*ContactPage.name, Name) 29 self.sendKeys(*ContactPage.mail, Mail) 30 if Star == '1': 31 self.click(*ContactPage.star) 32 self.sendKeys(*ContactPage.phone, Phone) 33 self.sendKeys(*ContactPage.comment, Comment) 34 self.click(*ContactPage.commit) 35 print('--------end add contact--------') 36 37 def assertErrorTip(self, excepted): 38 """斷言聯繫人添加失敗時是否有提示信息""" 39 text = self.getElementText(*ContactPage.errortip) 40 print('info: assert "{}"=="{}"'.format(text, excepted)) 41 assert text == excepted 42 43 if __name__ == '__main__': 44 from selenium import webdriver 45 from Page.PageObject.LoginPage import LoginPage 46 from Page.PageObject.HomePage import HomePage 47 driver = webdriver.Firefox() 48 home = HomePage(driver) 49 login = LoginPage(driver) 50 contact = ContactPage(driver) 51 52 login.login('賬號', 'xiaochao11520') 53 home.selectMenu() 54 contact.newContact('[email protected]')
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 9:16 4 @Auth : linux超 5 @File : SendMailPage.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 from Page.BasePage import BasePage 11 12 13 class SendMailPage(BasePage): 14 # 配置文件讀取元素 15 writeMail = BasePage.cf.getLocatorsOrAccount('SendMailPageElements', 'writeMail') 16 addressee = BasePage.cf.getLocatorsOrAccount('SendMailPageElements', 'addressee') 17 subject = BasePage.cf.getLocatorsOrAccount('SendMailPageElements', 'subject') 18 iframe = BasePage.cf.getLocatorsOrAccount('SendMailPageElements', 'iframe') 19 text = BasePage.cf.getLocatorsOrAccount('SendMailPageElements', 'text') 20 sendBtn = BasePage.cf.getLocatorsOrAccount('SendMailPageElements', 'sendBtn') 21 expect = BasePage.cf.getLocatorsOrAccount('SendMailPageElements', 'expect') 22 uploadAttachment = BasePage.cf.getLocatorsOrAccount('SendMailPageElements', 'uploadAttachment') 23 delete = BasePage.cf.getLocatorsOrAccount('SendMailPageElements', 'delete') 24 25 def sendMail(self, Address, Subject, Text, PFA=''): 26 """發送郵件功能""" 27 print('------------string send mail---------------------') 28 self.click(*SendMailPage.writeMail) 29 self.sendKeys(*SendMailPage.addressee, Address) 30 self.sendKeys(*SendMailPage.subject, Subject) 31 self.switchToFrame(*SendMailPage.iframe) 32 self.sendKeys(*SendMailPage.text, Text) 33 self.switchToDefaultFrame() 34 if PFA: 35 self.click(*SendMailPage.uploadAttachment) 36 self.ctrlV(PFA) 37 self.enterKey() 38 self.waitElementtobelocated(*SendMailPage.delete) 39 self.click(*SendMailPage.sendBtn) 40 print('------------end send mail---------------------') 41 42 if __name__=='__main__': 43 from Page.PageObject.LoginPage import LoginPage 44 from selenium import webdriver 45 driver = webdriver.Firefox() 46 47 login = LoginPage(driver) 48 login.login('賬號', 'xiaochao11520') 49 sendMail = SendMailPage(driver) 50 sendMail.sendMail('[email protected]', 'pytest', 'pytest實戰實例', 1, 'D:\KeyWordDriverTestFrameWork\geckodriver.log')
所有的準備工作都已經做好了,還有一個問題,我們的添加聯繫人和發送郵件應該是否應該在已經登錄的前提下測試呢?答案是肯定的。所以我們在用例同目錄下新建conftest.py文件並調用登錄功能(爲什麼這麼做,不明白的小夥伴可以去看一下我之前的文章,關於conftest.py的原理)
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 15:10 4 @Auth : linux超 5 @File : conftest.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 import pytest 11 from Page.PageObject.LoginPage import LoginPage 12 13 14 # 從配置文件中獲取正確的用戶名和密碼 15 userName = LoginPage.cf.getLocatorsOrAccount('126LoginAccount', 'username') 16 passWord = LoginPage.cf.getLocatorsOrAccount('126LoginAccount', 'password') 17 @pytest.fixture(scope='function') 18 def login(driver): 19 '''除登錄用例,每一個用例的前置條件''' 20 print('------------staring login------------') 21 loginFunc = LoginPage(driver, 30) 22 loginFunc.login(userName, passWord) 23 yield 24 print('------------end login------------') 25 driver.delete_all_cookies()
ok,開始編寫測試用例啦
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 14:10 4 @Auth : linux超 5 @File : test_loginCase.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 import pytest 11 from Page.PageObject.LoginPage import LoginPage 12 13 14 @pytest.mark.loginTest 15 class TestLogin(object): 16 17 # 測試數據 18 loginSheet = LoginPage.getSheet('login') 19 data = LoginPage.excel.getAllValuesOfSheet(loginSheet) 20 21 # 正確的帳號和密碼 22 userName = LoginPage.cf.getLocatorsOrAccount('126LoginAccount', 'username') 23 passWord = LoginPage.cf.getLocatorsOrAccount('126LoginAccount', 'password') 24 25 @pytest.fixture() 26 def teardown_func(self, driver): 27 """ 28 執行每個用例之後要清除一下cookie, 29 否則你第一個賬號登錄之後,重新加載網址還是登錄狀態,無法測試後面的賬號 30 """ 31 yield 32 driver.delete_all_cookies() 33 34 @pytest.mark.parametrize('username, password, expect', data) 35 def test_login(self, teardown_func, driver, username, password, expect): 36 """測試登錄""" 37 login = LoginPage(driver, 30) 38 login.login(username, password) 39 login.sleep(5) 40 # 增加登錄失敗時, 對提示信息的驗證 41 if username == TestLogin.userName and password == TestLogin.passWord: 42 login.assertValueInSource(expect) 43 elif username == '': 44 login.assertTextEqString(expect) 45 elif username != '' and password == '': 46 login.assertTextEqString(expect) 47 elif username == '' and password == '': 48 login.assertTextEqString(expect) 49 else: 50 login.assertTextEqString(expect) 51 52 53 if __name__ == "__main__": 54 pytest.main(['-v', 'test_loginCase.py'])
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 16:15 4 @Auth : linux超 5 @File : test_contactCase.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 import re 11 import pytest 12 from Page.PageObject.HomePage import HomePage 13 from Page.PageObject.ContactPage import ContactPage 14 15 16 @pytest.mark.conatctTest 17 class TestAddContact(object): 18 19 # 測試數據 20 contactSheet = ContactPage.getSheet('contact') 21 data = ContactPage.excel.getAllValuesOfSheet(contactSheet) 22 23 @pytest.mark.newcontact 24 @pytest.mark.parametrize('Name, Mail, Star, Phone, Comment, expect', data) 25 def test_NewContact(self, driver, login, Name, Mail, Star, Phone, Comment, expect): 26 """測試添加聯繫人""" 27 home_page = HomePage(driver) 28 contact_page = ContactPage(driver) 29 home_page.selectMenu() 30 contact_page.newContact(Name, Mail, Star, Phone, Comment) 31 home_page.sleep(5) 32 # 校驗錯誤的郵箱是否提示信息正確 33 if re.match(r'^.{1,}@[0-9a-zA-Z]{1,13}\..*$', Mail): 34 contact_page.assertValueInSource(expect) 35 else: 36 contact_page.assertErrorTip(expect) 37 38 if __name__ == '__main__': 39 pytest.main(['-v', 'test_contactCase.py'])
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 10:04 4 @Auth : linux超 5 @File : test_sendMailCase.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 import pytest 11 from Page.PageObject.SendMailPage import SendMailPage 12 13 @pytest.mark.sendMailTest 14 class TestSendMail(object): 15 16 sendMailSheet = SendMailPage.getSheet('mail') 17 data = SendMailPage.excel.getAllValuesOfSheet(sendMailSheet) 18 19 @pytest.mark.sendmail 20 @pytest.mark.parametrize('Address, Subject, Text, PFA', data) 21 def test_sendMail(self, driver, login, Address, Subject, Text,PFA): 22 """測試發送郵件,包括帶附件的郵件""" 23 send_mail = SendMailPage(driver) 24 send_mail.sendMail(Address, Subject, Text, PFA) 25 send_mail.sleep(5) 26 assert send_mail.isElementExsit(*SendMailPage.expect) 27 28 if __name__=='__main__': 29 pytest.main(['-v', 'test_sendMailCase.py'])
問題
用例已經寫完了,有兩個問題
1.有沒有發現我們的報告怎麼生成的?也沒有失敗用例截圖?
2.我們貌似並沒有編寫驅動瀏覽器的代碼?
現在我們來解決這個兩個問題
根據pytest的conftest.py文件的原理,我們可以把驅動瀏覽器的代碼寫在一個全局的conftest.py文件裏面。報告生成其實是通過命令 pytest --html=‘report.html’ --self-contained-html生成的,但是這樣的報告對用例的描述不是很清晰,且沒有對失敗用例截圖,也不方便我們分析項目的缺陷,我們也可以填寫代碼放到這個文件裏面(關於報告修改的文章)
1 """ 2 ------------------------------------ 3 @Time : 2019/4/12 14:10 4 @Auth : linux超 5 @File : conftest.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 import pytest 11 from selenium import webdriver 12 from py._xmlgen import html 13 14 _driver = None 15 # 測試失敗時添加截圖和測試用例描述(用例的註釋信息) 16 17 @pytest.mark.hookwrapper 18 def pytest_runtest_makereport(item): 19 """ 20 當測試失敗的時候,自動截圖,展示到html報告中 21 :param item: 22 """ 23 pytest_html = item.config.pluginmanager.getplugin('html') 24 outcome = yield 25 report = outcome.get_result() 26 extra = getattr(report, 'extra', []) 27 28 if report.when == 'call' or report.when == "setup": 29 xfail = hasattr(report, 'wasxfail') 30 if (report.skipped and xfail) or (report.failed and not xfail): 31 file_name = report.nodeid.replace("::", "_")+".png" 32 screen_img = _capture_screenshot() 33 if file_name: 34 html = '<div><img src="data:image/png;base64,%s" alt="screenshot" style="width:600px;height:300px;" ' \ 35 'onclick="window.open(this.src)" align="right"/></div>' % screen_img 36 extra.append(pytest_html.extras.html(html)) 37 report.extra = extra 38 report.description = str(item.function.__doc__) 39 report.nodeid = report.nodeid.encode("utf-8").decode("unicode_escape") 40 41 42 @pytest.mark.optionalhook 43 def pytest_html_results_table_header(cells): 44 cells.insert(1, html.th('Description')) 45 cells.insert(2, html.th('Test_nodeid')) 46 cells.pop(2) 47 48 49 @pytest.mark.optionalhook 50 def pytest_html_results_table_row(report, cells): 51 cells.insert(1, html.td(report.description)) 52 cells.insert(2, html.td(report.nodeid)) 53 cells.pop(2) 54 55 56 def _capture_screenshot(): 57 """ 58 截圖保存爲base64 59 :return: 60 """ 61 return _driver.get_screenshot_as_base64() 62 # 這裏我設置的級別是模塊級別,也就是每個測試文件運行一次 63 # 可以設置爲session,全部用例執行一次,但是針對126郵箱的話 64 # 登錄次數太多會叫你驗證,如果驗證就沒法執行用例了,我沒有對驗證處理(處理比較複雜) 65 66 67 @pytest.fixture(scope='module') 68 def driver(): 69 global _driver 70 print('------------open browser------------') 71 _driver = webdriver.Firefox() 72 73 yield _driver 74 print('------------close browser------------') 75 _driver.quit()
最後呢,爲了減小項目維護成本,我們把一些全局的配置項,放到我們的功能配置文件中共全局使用,包括運行用例的一些命令字符串,可以自行修改
1 """ 2 ------------------------------------ 3 @Time : 2019/4/20 16:50 4 @Auth : linux超 5 @File : conf.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 from datetime import datetime 11 import os 12 # 項目根目錄 13 projectDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 14 # 報告目錄 15 reportDir = os.path.join(projectDir, 'report') 16 # ui對象庫config.ini文件所在目錄 17 configDir = os.path.join(projectDir, 'config', 'config.ini') 18 # 測試數據所在目錄 19 excelPath = os.path.join(projectDir, 'data', 'tcData.xlsx') 20 # 當前時間 21 currentTime = datetime.now().strftime('%H_%M_%S') 22 23 # 郵件配置信息 24 # 郵件服務器 25 smtpServer = 'smtp.qq.com' 26 # 發送者 27 fromUser = '賬號@qq.com' 28 # 發送者密碼 29 fromPassWord = 'mhxvqpewblldbjhf' 30 # 接收者 31 toUser = ['賬號@qq.com']# 可以同時發送給多人,追加到列表中 32 # 郵件標題 33 subject = 'xx項目自動化測試報告' 34 # 郵件正文 35 contents = '測試報告正文' 36 # 報告名稱 37 htmlName = r'{}\testReport{}.html'.format(reportDir, currentTime) 38 39 # 腳本執行命令 40 args = r'pytest --html=' + htmlName+ ' ' + '--self-contained-html' 41 # modify by linuxchao at 2019/4/25 42 args_login = r'pytest --html='+ htmlName+ ' ' + '-m' + ' ' + 'loginTest'+ ' --self-contained-html' 43 args_contact = r'pytest --html='+ htmlName+ ' ' + '-m' + ' ' + 'contactTest'+ ' --self-contained-html' 44 args_sendmail = r'pytest --html='+ htmlName+ ' ' + '-m' + ' ' + 'sendMailTest'+ ' --self-contained-html'
運行項目
通過命令運行
1.cmd切換到項目的根目錄,執行pytest --html=‘report.html’ --self-contained-html命令(此運行方式,無法發送測試報告郵件)
這種方式感覺有點low,我們換另外一種方式,可以通過os模塊自動執行相關命令,編寫運行用例代碼
1 """ 2 ------------------------------------ 3 @Time : 2019/4/15 16:14 4 @Auth : linux超 5 @File : RunTestCase.py 6 @IDE : PyCharm 7 @Motto: Real warriors,dare to face the bleak warning,dare to face the incisive error! 8 ------------------------------------ 9 """ 10 import sys 11 sys.path.append('.') 12 from config.conf import * 13 from util.sendMailForReprot import SendMailWithReport 14 15 16 def main(): 17 # 判斷項目的根目錄是否在sys.path中,沒有就添加 18 if projectDir not in sys.path: 19 sys.path.append(projectDir) 20 # 執行用例 21 os.system(args) 22 # 發送郵件 23 SendMailWithReport.send_mail( 24 smtpServer, fromUser, fromPassWord, 25 toUser, subject, contents, 26 htmlName) 27 28 29 if __name__ == '__main__': 30 main()
我們可以直接執行這個文件執行所用的測試用例了!
其實我們運行用例往往不只是 使用pytest --html=‘report.html’ --self-contained-html 這樣一個簡單的命令運行,通常會添加很多的命令選項,比如-v,-q,-s等等,那麼怎麼辦呢?這時候就用到了pytest.ini配置文件了
只添加了幾個簡單的命令選項(pytest.ini具體使用及原理)
1 [pytest] 2 addopts=-vqs 3 testpaths=./TestCases 4 markers= 5 loginTest: Run login test cases 6 contactTest: Run add contact test cases 7 sendMailTest: Run send mail test cases
測試輸出
1.自動生成html格式報告,其中報告裏面附帶用例執行日誌明細,及用例失敗自動截圖(部分報告展示)
2.自動發送測試郵件給指定用戶
項目源碼
源碼在本人github上,可自行下載!
PS:最後還是附上我們的QQ交流羣:878565760 真心希望所有對測試感興趣,想入門,想提升自己測試能力的小夥伴加入!