1. 程式人生 > >基於python2+selenium3+pytest4的UI自動化框架

基於python2+selenium3+pytest4的UI自動化框架

環境:Python2.7.10, selenium3.141.0, pytest4.6.6, pytest-html1.22.0, Windows-7-6.1.7601-SP1

特點:
- 二次封裝了selenium,編寫Case更加方便。
- 採用PO設計思想,一個頁面一個Page.py,並在其中定義元素和操作方法;在TestCase中直接呼叫頁面中封裝好的操作方法操作頁面。
- 一次測試只啟動一次瀏覽器,節約時間提高效率(適合公司業務的才是最好的)。
- 增強pytest-html報告內容,加入失敗截圖、用例描述列、執行日誌。
- 支援命令列引數。
- 支援郵件傳送報告。

目錄結構:
- config

    - config.py:存放全域性變數,各種配置、driver等
- drive:各瀏覽器驅動檔案,如chromedriver.exe
- file
    - download:下載資料夾
    - screenshot:截圖資料夾
    - upload:上傳資料夾
- page_object:一個頁面一個.py,存放頁面物件、操作方法
    - base_page.py:基礎頁面,封裝了selenium的各種操作
    - hao123_page.py:hao123頁面
    - home_page.py:百度首頁
    - news_page.py:新聞首頁
    - search_page.py:搜尋結果頁
- report:
    - report.html:pytest-html生成的報告
- test_case
    - conftest.py:pytest特有檔案,在裡面增加了報告失敗截圖、用例描述列
    - test_home.py:百度首頁測試用例
    - test_news.py:新聞首頁測試用例
    - test_search.py:搜尋結果頁測試用例
- util:工具包
    - log.py:封裝了日誌模組
    - mail.py:封裝了郵件模組,使用傳送報告郵件功能需要先設定好相關配置,如使用者名稱密碼
- run.py:做為執行入口,封裝了pytest執行命令;實現所有測試用例共用一個driver;實現了執行引數化(結合Jenkins使用);log配置初始化;可配置傳送報告郵件。

程式碼實現:

 1 # coding=utf-8
 2 
 3 import os
 4 
 5 
 6 def init():
 7     global _global_dict
 8     _global_dict = {}
 9 
10     # 程式碼根目錄
11     root_dir = os.getcwd()
12 
13     # 存放程式所在目錄
14     _global_dict['root_path'] = root_dir
15     # 存放正常截圖資料夾
16     _global_dict['screenshot_path'] = "{}\\file\\screenshot\\".format(root_dir)
17     # 下載資料夾
18     _global_dict['download_path'] = "{}\\file\\download\\".format(root_dir)
19     # 上傳資料夾
20     _global_dict['upload_path'] = "{}\\file\\upload\\".format(root_dir)
21     # 存放報告路徑
22     _global_dict['report_path'] = "{}\\report\\".format(root_dir)
23 
24     # 儲存driver
25     _global_dict['driver'] = None
26 
27     # 設定執行環境網址主頁
28     _global_dict['site'] = 'https://www.baidu.com/'
29     # 執行環境,預設preview,可設為product
30     _global_dict['environment'] = 'preview'
31 
32 
33 def set_value(name, value):
34     """
35     修改全域性變數的值
36     :param name: 變數名
37     :param value: 變數值
38     """
39     _global_dict[name] = value
40 
41 
42 def get_value(name, def_val='no_value'):
43     """
44     獲取全域性變數的值
45     :param name: 變數名
46     :param def_val: 預設變數值
47     :return: 變數存在時返回其值,否則返回'no_value'
48     """
49     try:
50         return _global_dict[name]
51     except KeyError:
52         return def_val
config.py

 定義了全域性的字典,用來存放全域性變數,其key為變數名,value為變數值,可跨檔案、跨用例傳遞引數。

其中set_value、get_value分別用來存、取全域性變數。

 

 1 # coding=utf-8
 2 
 3 import logging
 4 import time
 5 import config.config as cf
 6 
 7 
 8 class Logger(object):
 9     """封裝的日誌模組"""
10 
11     def __init__(self, logger, cmd_level=logging.DEBUG, file_level=logging.DEBUG):
12         try:
13             self.logger = logging.getLogger(logger)
14             self.logger.setLevel(logging.DEBUG)  # 設定日誌輸出的預設級別
15             '''pytest報告可以自動將log整合進報告,不用再自己單獨設定儲存
16             # 日誌輸出格式
17             fmt = logging.Formatter(
18                 '%(asctime)s[%(levelname)s]\t%(message)s')
19             # 日誌檔名稱
20             curr_time = time.strftime("%Y-%m-%d %H.%M.%S")
21             log_path = cf.get_value('log_path')
22             self.log_file = '{}log{}.txt'.format(log_path, curr_time)
23             # 設定控制檯輸出
24             sh = logging.StreamHandler()
25             sh.setFormatter(fmt)
26             sh.setLevel(cmd_level)
27             # 設定檔案輸出
28             fh = logging.FileHandler(self.log_file)
29             fh.setFormatter(fmt)
30             fh.setLevel(file_level)
31             # 新增日誌輸出方式
32             self.logger.addHandler(sh)
33             self.logger.addHandler(fh)
34             '''
35         except Exception as e:
36             raise e
37 
38     def debug(self, msg):
39         self.logger.debug(msg)
40 
41     def info(self, msg):
42         self.logger.info(msg)
43 
44     def error(self, msg):
45         self.logger.error(msg)
46 
47     def warning(self, msg):
48         self.logger.warning(msg)
log.py

封裝的log模組

 

 1 # coding=utf-8
 2 
 3 import smtplib
 4 from email.mime.text import MIMEText
 5 from email.mime.multipart import MIMEMultipart
 6 from email.header import Header
 7 import config.config as cf
 8 
 9 
10 def send_mail(sendto):
11     """
12     傳送郵件
13     :param sendto:收件人列表,如['[email protected]']
14     """
15     mail_host = 'smtp.sohu.com'  # 郵箱伺服器地址
16     username = '[email protected]'  # 郵箱使用者名稱
17     password = 'test'  # 郵箱密碼
18     receivers = sendto  # 收件人
19 
20     # 建立一個帶附件的例項
21     message = MIMEMultipart()
22     message['From'] = Header(u'UI自動化', 'utf-8')
23     message['subject'] = Header(u'UI自動化測試結果', 'utf-8')  # 郵件標題
24     message.attach(MIMEText(u'測試結果詳見附件', 'plain', 'utf-8'))# 郵件正文
25     # 構造附件
26     report_root = cf.get_value('report_path')  # 獲取報告路徑
27     report_file = 'report.html'  # 報告檔名稱
28     att1 = MIMEText(open(report_root + report_file, 'rb').read(), 'base64', 'utf-8')
29     att1["Content-Type"] = 'application/octet-stream'
30     att1["Content-Disposition"] = 'attachment; filename={}'.format(report_file)
31     message.attach(att1)
32 
33     try:
34         smtp = smtplib.SMTP()
35         smtp.connect(mail_host, 25)  # 25為 SMTP 埠號
36         smtp.login(username, password)
37         smtp.sendmail(username, receivers, message.as_string())
38         print u'郵件傳送成功'
39     except Exception, e:
40         print u'郵件傳送失敗'
41         raise e
mail.py

封裝的郵件模組,報告HTML檔案會做為附件傳送,這裡需要把最上面的4個變數全改成你自己的。

 

  1 # coding=utf-8
  2 
  3 from selenium.common.exceptions import TimeoutException
  4 from selenium.webdriver.support.ui import WebDriverWait
  5 from selenium.webdriver.common.keys import Keys
  6 from selenium.webdriver.common.action_chains import ActionChains
  7 import os
  8 import inspect
  9 import config.config as cf
 10 import logging
 11 import time
 12 
 13 log = logging.getLogger('szh.BasePage')
 14 
 15 
 16 class BasePage(object):
 17     def __init__(self):
 18         self.driver = cf.get_value('driver')  # 從全域性變數取driver
 19 
 20     def split_locator(self, locator):
 21         """
 22         分解定位表示式,如'css,.username',拆分後返回'css selector'和定位表示式'.username'(class為username的元素)
 23         :param locator: 定位方法+定位表示式組合字串,如'css,.username'
 24         :return: locator_dict[by], value:返回定位方式和定位表示式
 25         """
 26         by = locator.split(',')[0]
 27         value = locator.split(',')[1]
 28         locator_dict = {
 29             'id': 'id',
 30             'name': 'name',
 31             'class': 'class name',
 32             'tag': 'tag name',
 33             'link': 'link text',
 34             'plink': 'partial link text',
 35             'xpath': 'xpath',
 36             'css': 'css selector',
 37         }
 38         if by not in locator_dict.keys():
 39             raise NameError("wrong locator!'id','name','class','tag','link','plink','xpath','css',exp:'id,username'")
 40         return locator_dict[by], value
 41 
 42     def wait_element(self, locator, sec=30):
 43         """
 44         等待元素出現
 45         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
 46         :param sec:等待秒數
 47         """
 48         by, value = self.split_locator(locator)
 49         try:
 50             WebDriverWait(self.driver, sec, 1).until(lambda x: x.find_element(by=by, value=value),
 51                                                      message='element not found!!!')
 52             log.info(u'等待元素:%s' % locator)
 53             return True
 54         except TimeoutException:
 55             return False
 56         except Exception, e:
 57             raise e
 58 
 59     def get_element(self, locator, sec=60):
 60         """
 61         獲取一個元素
 62         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
 63         :param sec:等待秒數
 64         :return: 元素可找到返回element物件,否則返回False
 65         """
 66         if self.wait_element(locator, sec):
 67             by, value = self.split_locator(locator)
 68             print by, value
 69             try:
 70                 element = self.driver.find_element(by=by, value=value)
 71                 log.info(u'獲取元素:%s' % locator)
 72                 return element
 73             except Exception, e:
 74                 raise e
 75         else:
 76             return False
 77 
 78     def get_elements(self, locator):
 79         """
 80         獲取一組元素
 81         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
 82         :return: elements
 83         """
 84         by, value = self.split_locator(locator)
 85         try:
 86             elements = WebDriverWait(self.driver, 60, 1).until(lambda x: x.find_elements(by=by, value=value))
 87             log.info(u'獲取元素列表:%s' % locator)
 88             return elements
 89         except Exception, e:
 90             raise e
 91 
 92     def open(self, url):
 93         """
 94         開啟網址
 95         :param url: 網址連線
 96         """
 97         self.driver.get(url)
 98         log.info(u'開啟網址:%s' % url)
 99 
100     def clear(self, locator):
101         """
102         清除元素中的內容
103         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
104         """
105         self.get_element(locator).clear()
106         log.info(u'清空內容:%s' % locator)
107 
108     def type(self, locator, text):
109         """
110         在元素中輸入內容
111         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
112         :param text: 輸入的內容
113         """
114         self.get_element(locator).send_keys(text)
115         log.info(u'向元素 %s 輸入文字:%s' % (locator, text))
116 
117     def enter(self, locator):
118         """
119         在元素上按回車鍵
120         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
121         """
122         self.get_element(locator).send_keys(Keys.ENTER)
123         log.info(u'在元素 %s 上按回車' % locator)
124 
125     def click(self, locator):
126         """
127         在元素上單擊
128         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
129         """
130         self.get_element(locator).click()
131         log.info(u'點選元素:%s' % locator)
132 
133     def right_click(self, locator):
134         """
135         滑鼠右擊元素
136         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
137         """
138         element = self.get_element(locator)
139         ActionChains(self.driver).context_click(element).perform()
140         log.info(u'在元素上右擊:%s' % locator)
141 
142     def double_click(self, locator):
143         """
144         雙擊元素
145         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
146         """
147         element = self.get_element(locator)
148         ActionChains(self.driver).double_click(element).perform()
149         log.info(u'在元素上雙擊:%s' % locator)
150 
151     def move_to_element(self, locator):
152         """
153         滑鼠指向元素
154         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
155         """
156         element = self.get_element(locator)
157         ActionChains(self.driver).move_to_element(element).perform()
158         log.info(u'指向元素%s' % locator)
159 
160     def drag_and_drop(self, locator, target_locator):
161         """
162         拖動一個元素到另一個元素位置
163         :param locator: 要拖動元素的定位
164         :param target_locator: 目標位置元素的定位
165         """
166         element = self.get_element(locator)
167         target_element = self.get_element(target_locator)
168         ActionChains(self.driver).drag_and_drop(element, target_element).perform()
169         log.info(u'把元素 %s 拖至元素 %s' % (locator, target_locator))
170 
171     def drag_and_drop_by_offset(self, locator, xoffset, yoffset):
172         """
173         拖動一個元素向右下移動x,y個偏移量
174         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
175         :param xoffset: X offset to move to
176         :param yoffset: Y offset to move to
177         """
178         element = self.get_element(locator)
179         ActionChains(self.driver).drag_and_drop_by_offset(element, xoffset, yoffset).perform()
180         log.info(u'把元素 %s 拖至座標:%s %s' % (locator, xoffset, yoffset))
181 
182     def click_link(self, text):
183         """
184         按部分連結文字查詢並點選連結
185         :param text: 連結的部分文字
186         """
187         self.get_element('plink,' + text).click()
188         log.info(u'點選連線:%s' % text)
189 
190     def alert_text(self):
191         """
192         返回alert文字
193         :return: alert文字
194         """
195         log.info(u'獲取彈框文字:%s' % self.driver.switch_to.alert.text)
196         return self.driver.switch_to.alert.text
197 
198     def alert_accept(self):
199         """
200         alert點確認
201         """
202         self.driver.switch_to.alert.accept()
203         log.info(u'點選彈框確認')
204 
205     def alert_dismiss(self):
206         """
207         alert點取消
208         """
209         self.driver.switch_to.alert.dismiss()
210         log.info(u'點選彈框取消')
211 
212     def get_attribute(self, locator, attribute):
213         """
214         返回元素某屬性的值
215         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
216         :param attribute: 屬性名稱
217         :return: 屬性值
218         """
219         value = self.get_element(locator).get_attribute(attribute)
220         log.info(u'獲取元素 %s 的屬性值 %s 為:%s' % (locator, attribute, value))
221         return value
222 
223     def get_ele_text(self, locator):
224         """
225         返回元素的文字
226         :param locator: 定位方法+定位表示式組合字串,用逗號分隔,如'css,.username'
227         :return: 元素的文字
228         """
229         log.info(u'獲取元素 %s 的文字為:%s' % (locator, self.get_element(locator).text))
230         return self.get_element(locator).text
231 
232     def frame_in(self, locator):
233         """
234         進入frame
235         :param locator: 定位方法+定位表示式組合字串,如'css,.username'
236         """
237         e = self.get_element(locator)
238         self.driver.switch_to.frame(e)
239         log.info(u'進入frame:%s' % locator)
240 
241     def frame_out(self):
242         """
243         返回主文件
244         """
245         self.driver.switch_to.default_content()
246         log.info(u'退出frame返回預設文件')
247 
248     def open_new_window_by_locator(self, locator):
249         """
250         點選元素開啟新視窗,並將控制代碼切換到新視窗
251         :param locator: 定位方法+定位表示式組合字串,如'css,.username'
252         """
253         self.get_element(locator).click()
254         self.driver.switch_to.window(self.driver.window_handles[-1])
255         log.info(u'點選元素 %s 開啟新視窗' % locator)
256 
257         # old_handle = self.driver.current_window_handle
258         # self.get_element(locator).click()
259         # all_handles = self.driver.window_handles
260         # for handle in all_handles:
261         #     if handle != old_handle:
262         #         self.driver.switch_to.window(handle)
263 
264     def open_new_window_by_element(self, element):
265         """
266         點選元素開啟新視窗,並將控制代碼切換到新視窗
267         :param element: 元素物件
268         """
269         element.click()
270         self.driver.switch_to.window(self.driver.window_handles[-1])
271         log.info(u'點選元素開啟新視窗')
272 
273     def js(self, script):
274         """
275         執行JavaScript
276         :param script:js語句 
277         """
278         self.driver.execute_script(script)
279         log.info(u'執行JS語句:%s' % script)
280 
281     def scroll_element(self, locator):
282         """
283         拖動滾動條至目標元素
284         :param locator: 定位方法+定位表示式組合字串,如'css,.username'
285         """
286         script = "return arguments[0].scrollIntoView();"
287         element = self.get_element(locator)
288         self.driver.execute_script(script, element)
289         log.info(u'滾動至元素:%s' % locator)
290 
291     def scroll_top(self):
292         """
293         滾動至頂部
294         """
295         self.js("window.scrollTo(document.body.scrollHeight,0)")
296         log.info(u'滾動至頂部')
297 
298     def scroll_bottom(self):
299         """
300         滾動至底部
301         """
302         self.js("window.scrollTo(0,document.body.scrollHeight)")
303         log.info(u'滾動至底部')
304 
305     def back(self):
306         """
307         頁面後退
308         """
309         self.driver.back()
310         log.info(u'頁面後退')
311 
312     def forward(self):
313         """
314         頁面向前
315         """
316         self.driver.forward()
317         log.info(u'頁面向前')
318 
319     def is_text_on_page(self, text):
320         """
321         返回頁面原始碼
322         :return: 頁面原始碼
323         """
324         if text in self.driver.page_source:
325             log.info(u'判斷頁面上有文字:%s' % text)
326             return True
327         else:
328             log.info(u'判斷頁面上沒有文字:%s' % text)
329             return False
330 
331     def refresh(self):
332         """
333         重新整理頁面
334         """
335         self.driver.refresh()
336         log.info(u'重新整理頁面')
337 
338     def screenshot(self, info='-'):
339         """
340         截圖,起名為:檔名-方法名-註釋
341         :param info: 截圖說明
342         """
343         catalog_name = cf.get_value('screenshot_path')  # 從全域性變數取截圖資料夾位置
344         if not os.path.exists(catalog_name):
345             os.makedirs(catalog_name)
346         class_object = inspect.getmembers(inspect.stack()[1][0])[-3][1]['self']  # 獲得測試類的object
347         classname = str(class_object).split('.')[1].split(' ')[0]  # 獲得測試類名稱
348         testcase_name = inspect.stack()[1][3]  # 獲得測試方法名稱
349         filepath = catalog_name + classname + "@" + testcase_name + info + ".png"
350         self.driver.get_screenshot_as_file(filepath)
351         log.info(u'截圖:%s.png' % info)
352 
353     def close(self):
354         """
355         關閉當前頁
356         """
357         self.driver.close()
358         self.driver.switch_to.window(self.driver.window_handles[0])
359         log.info(u'關閉當前Tab')
360 
361     def sleep(self, sec):
362         time.sleep(sec)
363         log.info(u'等待%s秒' % sec)
base_page.py

二次封裝了selenium常用操作,做為所有頁面類的基類。

本框架支援selenium所有的定位方法,為了提高編寫速度,改進了使用方法,定義元素時方法名和方法值為一個用逗號隔開的字串,如:
- xpath定位:i_keyword = 'xpath,//input[@id="kw"]' # 關鍵字輸入框
- id定位:b_search = 'id,su' # 搜尋按鈕
- 其他定位方法同上,不再一一舉例

使用時如上面程式碼中type()方法,是在如輸入框中輸入文字,呼叫時輸入type(i_keyword, "輸入內容")

type()中會呼叫get_element()方法,對輸入的定位表示式進行解析,並且會等待元素一段時間,當元素出現時立即進行操作。

 

另外可以看到每個基本操作都加入了日誌,下圖即是用例執行後報告中記錄的日誌

 

  

 1 # coding=utf-8
 2 
 3 from page_object.base_page import BasePage
 4 
 5 
 6 class SearchPage(BasePage):
 7     def __init__(self, driver):
 8         self.driver = driver
 9 
10     # i=輸入框, l=連結, im=圖片, t=文字控制元件, d=div, lab=label
11     # 含_百度百科的搜尋結果
12     l_baike = 'xpath,//a[(. = "星空物語_百度百科")]'
13 
14     # 下一頁
15     b_next_page = 'link,下一頁>'
16 
17     # 上一頁
18     b_up_page = 'xpath,//a[(. = "<上一頁")]'
19 
20     # 點選搜尋結果的百科
21     def click_result(self):
22         self.open_new_window_by_locator(self.l_baike)
23         self.sleep(3)
24 
25     # 點選下一頁
26     def click_next_page(self):
27         self.click(self.b_next_page)
search_page.py

PO模式中封裝的百度的搜尋頁,繼承了上面的BasePage類;每個頁面類中上面定義各控制元件的表示式,下面將頁面上的各種操作封裝為方法。這樣如果在多個用例中呼叫了控制元件或操作方法,將來更新維護只需要在頁面類中改一下,所有用例就都更新了。

 

 1 # coding=utf-8
 2 
 3 import sys
 4 reload(sys)
 5 sys.setdefaultencoding('utf8')
 6 from page_object.home_page import HomePage
 7 from page_object.search_page import SearchPage
 8 import pytest
 9 import config.config as cf
10 
11 
12 class TestSearch():
13     """
14     pytest:
15     測試檔案以test_開頭
16     測試類以Test開頭,並且不能帶有__init__方法
17     測試函式以test_開頭
18     斷言使用assert
19     """
20     driver = cf.get_value('driver')  # 從全域性變數取driver
21     home_page = HomePage(driver)
22     search_page = SearchPage(driver)
23 
24     def test_click_result(self):
25         """搜尋頁-點選首個搜尋結果"""
26         try:
27             self.home_page.open_homepage()
28             self.home_page.input_keyword(u'星空物語')  # 輸入關鍵字
29             self.search_page.click_result()  # 點選百科
30             assert self.home_page.is_text_on_page(u'電視劇《一起來看流星雨》片頭曲')  # 驗證頁面開啟
31             self.home_page.screenshot(u'開啟搜尋結果')
32             self.search_page.close()  # 關閉百科頁面
33         except Exception, e:
34             self.home_page.screenshot(u'開啟搜尋結果失敗')
35             raise e
36 
37     def test_click_next_page(self):
38         """搜尋頁-搜尋翻頁"""
39         try:
40             self.search_page.click_next_page()  # 點下一頁
41             assert self.home_page.wait_element(self.search_page.b_up_page)  # 上一頁出現
42             self.search_page.scroll_element(self.search_page.b_up_page)  # 滾到上一頁
43             self.home_page.screenshot(u'搜尋翻頁')
44         except Exception, e:
45             self.home_page.screenshot(u'搜尋翻頁失敗')
46             raise e
test_search.py

百度搜索頁的測試用例,這裡我簡單寫了2個用例,第1個是搜尋後點擊首個搜尋結果可開啟,第2個是搜尋結果可翻頁。用例中的具體操作均是使用的上面頁面類中封裝好的操作方法。

 

 1 # coding=utf-8
 2 
 3 import pytest
 4 from py._xmlgen import html
 5 import config.config as cf
 6 import logging
 7 
 8 log = logging.getLogger('szh.conftest')
 9 
10 
11 @pytest.mark.hookwrapper
12 def pytest_runtest_makereport(item):
13     """當測試失敗的時候,自動截圖,展示到html報告中"""
14     pytest_html = item.config.pluginmanager.getplugin('html')
15     outcome = yield
16     report = outcome.get_result()
17     extra = getattr(report, 'extra', [])
18 
19     if report.when == 'call' or report.when == "setup":
20         xfail = hasattr(report, 'wasxfail')
21         if (report.skipped and xfail) or (report.failed and not xfail):
22             file_name = report.nodeid.replace("::", "_") + ".png"
23             driver = cf.get_value('driver')  # 從全域性變數取driver
24             screen_img = driver.get_screenshot_as_base64()
25             if file_name:
26                 html = '<div><img src="data:image/png;base64,%s" alt="screenshot" style="width:600px;height:300px;" ' \
27                        'onclick="window.open(this.src)" align="right"/></div>' % screen_img
28                 extra.append(pytest_html.extras.html(html))
29         report.extra = extra
30         report.description = str(item.function.__doc__)#.decode('utf-8', 'ignore')  # 不解碼轉成Unicode,生成HTML會報錯
31         # report.nodeid = report.nodeid.encode("utf-8").decode("unicode_escape")
32 
33 
34 @pytest.mark.optionalhook
35 def pytest_html_results_table_header(cells):
36     cells.insert(1, html.th('Description'))
37     cells.pop()  # 刪除報告最後一列links
38 
39 
40 @pytest.mark.optionalhook
41 def pytest_html_results_table_row(report, cells):
42     cells.insert(1, html.td(report.description))
43     cells.pop()  # 刪除報告最後一列links
conftest.py

conftest.py是pytest提供資料、操作共享的檔案,其檔名是固定的,不可以修改。

conftest.py檔案所在目錄必須存在__init__.py檔案。

其他檔案不需要import匯入conftest.py,pytest用例會自動查詢

所有同目錄測試檔案執行前都會執行conftest.py檔案

我只在conftest.py中加入了報錯截圖的功能,如果你有需要在用例前、後執行一些操作,都可以寫在這裡。

 

 1 # coding=utf-8
 2 
 3 import pytest
 4 import config.config as cf
 5 from util.log import Logger
 6 import argparse
 7 from selenium import webdriver
 8 from util.mail import send_mail
 9 
10 
11 def get_args():
12     """命令列引數解析"""
13     parser = argparse.ArgumentParser(description=u'可選擇引數:')
14     parser.add_argument('-e', '--environment', choices=['preview', 'product'], default='preview', help=u'測試環境preview,線上環境product')
15     args = parser.parse_args()
16     if args.environment in ('pre', 'preview'):
17         cf.set_value('environment', 'preview')
18         cf.set_value('site', 'http://www.baidu.com/')
19     elif args.environment in ('pro', 'product'):
20         cf.set_value('environment', 'preview')
21         cf.set_value('site', 'https://www.baidu.com/')
22     else:
23         print u"請輸入preview/product"
24         exit()
25 
26 
27 def set_driver():
28     """設定driver"""
29     # 配置Chrome Driver
30     chrome_options = webdriver.ChromeOptions()
31     chrome_options.add_argument('--start-maximized')  # 瀏覽器最大化
32     chrome_options.add_argument('--disable-infobars')  # 不提醒chrome正在受自動化軟體控制
33     prefs = {'download.default_directory': cf.get_value('download_path')}
34     chrome_options.add_experimental_option('prefs', prefs)  # 設定預設下載路徑
35     # chrome_options.add_argument(r'--user-data-dir=D:\ChromeUserData')  # 設定使用者資料夾,可免登陸
36     driver = webdriver.Chrome('{}\\driver\\chromedriver.exe'.format(cf.get_value('root_path')), options=chrome_options)
37     cf.set_value('driver', driver)
38 
39 
40 def main():
41     """執行pytest命令啟動測試"""
42     pytest.main(['-v', '-s', 'test_case/', '--html=report/report.html', '--self-contained-html'])
43 
44 
45 if __name__ == '__main__':
46     cf.init()  # 初始化全域性變數
47     get_args()  # 命令列引數解析
48     log = Logger('szh')  # 初始化log配置
49     set_driver()  # 初始化driver
50     main()  # 執行pytest測試集
51     cf.get_value('driver').quit()  # 關閉selenium driver
52 
53     # 先將util.mail檔案send_mail()中的使用者名稱、密碼填寫正確,再啟用傳送郵件功能!!!
54     send_mail(['[email protected]'])  # 將報告發送至郵箱
run.py

run.py用來做一些初始化的工作,執行測試,以及測試收尾,具體可以看程式碼中的註釋。

我將瀏覽器driver的初始化放在了這裡,並將driver存入全域性變數,這樣瀏覽器只需開啟一次即可執行所有的測試。如果你想每個用例都開啟、關閉一次瀏覽器,那可以把定義driver的方法放在conftest.py中。

 

get_args()是封裝的命令列引數解析,方便整合Jenkins時快速定義執行內容。目前只定義了一個環境引數-e, 可設定測試環境preview,線上環境product,你可以根據需要新增更多引數。

呼叫方法:python run.py -e product

 

main()封裝了pytest的命令列執行模式,你也可以按需修改。

 

最後放一張執行後的測試報告的截圖,我故意將某個用例寫錯,可以看到,報告中顯示了具體的報錯資訊以及出錯時頁面的截圖

 

所有程式碼可去GitHub獲取:https://github.com/songzhenhua/selenium_ui_auto