环境: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:收件人列表,如['22459496@qq.com']
14     """
15     mail_host = 'smtp.sohu.com'  # 邮箱服务器地址
16     username = 'test@sohu.com'  # 邮箱用户名
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个变量全改成你自己的。

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(['22459496@qq.com'])  # 将报告发送至邮箱

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

def type(self, locator, text):
"""
在元素中输入内容
:param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
:param text: 输入的内容
"""
self.get_element(locator).send_keys(text)
log.info(u'向元素 %s 输入文字:%s' % (locator, text))

版权声明:本文为songzhenhua原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/songzhenhua/p/12317518.html