一.环境配置

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

二.特点

1.二次封装了selenium,编写Case更加方便

2.采用PO设计思想,一个页面一个Page.py,在其中定义元素和操作方法;从而在用例TestCase中直接调用页面中封装好的操作方法操作页面

3.一次测试只启动一次浏览器,节约时间提高效率

4.增强pytest-html报告内容,加入失败截图、用例描述列、运行日志

5.支持命令行参数

6.支持邮件发送报告

三.目录结构

在这里插入图片描述

1.config配置

1.1.config.py:存放全局变量,各种配置、driver等。

2.drive文件

2.1.各浏览器驱动文件,如chromedriver.exe

3.file文件

3.1.download:下载文件夹
3.2.screenshot:截图文件夹
3.3.upload:上传文件夹

4.page_object文件:一个页面一个.py,存放页面对象、操作方法

4.1.base_page.py:基础页面,封装selenium的各种操作

4.2.hao123_page.py:hao123页面

4.3.home_page.py:百度首页

4.4.news_page.py:新闻首页

4.5.search_page.py:搜索结果页

5.report文件

5.1.report.html:pytest-html生成的报告

6.test_case文件

6.1.conftest.py:pytest特有文件,在里面增加了报告失败截图、用例描述列

6.2.test_home.py:百度首页测试用例

6.3.test_news.py:新闻首页测试用例

6.4.test_search.py:搜索结果页测试用例

6.5.demo1.py:通过加cookies免登陆的Demo

6.6.demo2.py:在已打开的Chrome浏览器进行调试的Demo

7.util工具包文件

7.1.log.py:封装了日志模块

7.2.mail.py:封装了邮件模块,使用发送报告邮件功能需要先设置好相关配置,如用户名密码

8.run.py文件

8.1.封装pytest运行命令;实现所有测试用例共用driver;实现运行参数化(结合Jenkins使用);log配置初始化;可配置发送报告邮件

四.代码实现

1.config.py

1.1.定义了全局的字典,来存放全局变量。其key为变量名,value为变量值,可跨文件、跨用例传递参数。

1.2.其中set_value、get_value分别用来存、取全局变量
import os

def init():
    # 1.定义了全局的字典,用来存放全局变量,其key为变量名,value为变量值,可跨文件、跨用例传递参数。
    # 2.其中set_value、get_value分别用来存、取全局变量。
    global _global_dict
    _global_dict = {}

    # 代码根目录
    root_dir = os.getcwd()
    print(root_dir)

    # 存放程序所在目录
    _global_dict['root_path'] = root_dir
    # 存放正常截图文件夹
    _global_dict['screenshot_path'] = "{}\\file\\screenshot\\".format(root_dir)
    # 下载文件夹
    _global_dict['download_path'] = "{}\\file\\download\\".format(root_dir)
    # 上传文件夹
    _global_dict['upload_path'] = "{}\\file\\upload\\".format(root_dir)
    # 存放报告路径
    _global_dict['report_path'] = "{}\\report\\".format(root_dir)

    # 保存driver
    _global_dict['driver'] = None
    #_global_dict['driver'] = "{}\\driver\\".format(root_dir)

    # 设置运行环境网址主页
    _global_dict['site'] = 'https://www.baidu.com/'
    # 运行环境,默认preview,可设为product
    _global_dict['environment'] = 'preview'

def set_value(name, value):
    """
    存放全局变量的值
    :param name: 变量名
    :param value: 变量值
    """
    _global_dict[name] = value


def get_value(name, def_val='no_value'):
    """
    获取全局变量的值
    :param name: 变量名
    :param def_val: 默认变量值
    :return: 变量存在时返回其值,否则返回'no_value'
    """
    try:
        return _global_dict[name]
    except KeyError:
        return def_val

if __name__ == '__main__':
    init()

2.log.py

import logging
import time
import config.config as cf


class Logger(object):
    """封装的日志模块"""

    def __init__(self, logger, cmd_level=logging.DEBUG, file_level=logging.DEBUG):
        try:
            self.logger = logging.getLogger(logger)
            self.logger.setLevel(logging.DEBUG)  # 设置日志输出的默认级别

            '''pytest报告可以自动将log整合进报告,不用再自己单独设置保存
            # 日志输出格式
            fmt = logging.Formatter(
                '%(asctime)s[%(levelname)s]\t%(message)s')
            # 日志文件名称
            curr_time = time.strftime("%Y-%m-%d %H.%M.%S")
            log_path = cf.get_value('log_path')
            self.log_file = '{}log{}.txt'.format(log_path, curr_time)
            # 设置控制台输出
            sh = logging.StreamHandler()
            sh.setFormatter(fmt)
            sh.setLevel(cmd_level)
            # 设置文件输出
            fh = logging.FileHandler(self.log_file)
            fh.setFormatter(fmt)
            fh.setLevel(file_level)
            # 添加日志输出方式
            self.logger.addHandler(sh)
            self.logger.addHandler(fh)
            '''
        except Exception as e:
            raise e

    def debug(self, msg):
        self.logger.debug(msg)

    def info(self, msg):
        self.logger.info(msg)

    def error(self, msg):
        self.logger.error(msg)

    def warning(self, msg):
        self.logger.warning(msg)

3.mail.py

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
import config.config as cf

def send_mail(sendto):
    """
    发送邮件
    :param sendto:收件人列表,如['22459496@qq.com']
    """
    mail_host = 'smtp.sohu.com'  # 邮箱服务器地址
    username = 'test@sohu.com'  # 邮箱用户名
    password = 'test'  # 邮箱密码
    receivers = sendto  # 收件人

    # 创建一个带附件的实例
    message = MIMEMultipart()
    message['From'] = Header(u'UI自动化', 'utf-8')
    message['subject'] = Header(u'UI自动化测试结果', 'utf-8')  # 邮件标题
    message.attach(MIMEText(u'测试结果详见附件', 'plain', 'utf-8'))# 邮件正文
    # 构造附件
    report_root = cf.get_value('report_path')  # 获取报告路径
    report_file = 'report.html'  # 报告文件名称
    att1 = MIMEText(open(report_root + report_file, 'rb').read(), 'base64', 'utf-8')
    att1["Content-Type"] = 'application/octet-stream'
    att1["Content-Disposition"] = 'attachment; filename={}'.format(report_file)
    message.attach(att1)

    try:
        smtp = smtplib.SMTP()
        smtp.connect(mail_host, 25)  # 25为 SMTP 端口号
        smtp.login(username, password)
        smtp.sendmail(username, receivers, message.as_string())
        print u'邮件发送成功'
    except Exception, e:
        print u'邮件发送失败'
        raise e

4.base_page.py

from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
import os
import inspect
import config.config as cf
import logging
import time

log = logging.getLogger('szh.BasePage')


class BasePage(object):
    def __init__(self):
        self.driver = cf.get_value('driver')  # 从全局变量取driver
        #global driver

    def split_locator(self, locator):
        """
        分解定位表达式,如'css,.username',拆分后返回'css selector'和定位表达式'.username'(class为username的元素)
        :param locator: 定位方式+定位表达式组合字符串,如'css,.username'
        :return: locator_dict[by], value:返回定位方式和定位表达式
        """
        by = locator.split(',', 1)[0]
        value = locator.split(',', 1)[1]
        locator_dict = {
            'id': 'id',
            'name': 'name',
            'class': 'class name',
            'tag': 'tag name',
            'link': 'link text',
            'plink': 'partial link text',
            'xpath': 'xpath',
            'css': 'css selector',
        }
        if by not in locator_dict.keys():
            raise NameError("wrong locator!'id','name','class','tag','link','plink','xpath','css',exp:'id,username'")
        return locator_dict[by], value

    def wait_element(self, locator, sec=30):
        """
        等待元素出现
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        :param sec:等待秒数
        """
        by, value = self.split_locator(locator)
        try:
            WebDriverWait(self.driver, sec, 1).until(lambda x: x.find_element(by=by, value=value),
                                                     message='element not found!!!')
            log.info(u'等待元素:%s' % locator)
            return True
        except TimeoutException:
            return False
        except Exception, e:
            raise e

    def get_element(self, locator, sec=60):
        """
        获取一个元素
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        :param sec:等待秒数
        :return: 元素可找到返回element对象,否则返回False
        """
        if self.wait_element(locator, sec):
            by, value = self.split_locator(locator)
            try:
                element = self.driver.find_element(by=by, value=value)
                log.info(u'获取元素:%s' % locator)
                return element
            except Exception, e:
                raise e
        else:
            return False

    def get_elements(self, locator):
        """
        获取一组元素
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        :return: elements
        """
        by, value = self.split_locator(locator)
        try:
            elements = WebDriverWait(self.driver, 60, 1).until(lambda x: x.find_elements(by=by, value=value))
            log.info(u'获取元素列表:%s' % locator)
            return elements
        except Exception, e:
            raise e

    def get_url(self):
        """
        获取当前网址
        :return: 网址连接
        """
        log.info(u'获取当前网址:%s' % self.driver.current_url)
        return self.driver.current_url

    def open(self, url):
        """
        打开网址
        :param url: 网址连接
        """
        self.driver.get(url)
        log.info(u'打开网址:%s' % url)

    def clear(self, locator):
        """
        清除元素中的内容
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        """
        self.get_element(locator).clear()
        log.info(u'清空内容:%s' % locator)

    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))

    def type_all(self, locator, text):
        """
        在符合条件的所有元素中输入内容,依次循环输入text1,text2……
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        :param text: 输入的内容
        """
        allt = self.get_elements(locator)
        i = 1
        log.info(u'开始执行type_all,共%s个元素' % (len(allt)))
        for ele in allt:
            newtext = text + str(i)
            ele.send_keys(newtext)
            log.info(u'向第 %s 个元素输入文字:%s' % (i, newtext))
            i += 1

    def enter(self, locator):
        """
        在元素上按回车键
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        """
        self.get_element(locator).send_keys(Keys.ENTER)
        log.info(u'在元素 %s 上按回车' % locator)

    def click(self, locator, repeat=0):
        """
        在元素上单击
        :param repeat: 重复次数标记,不要填写
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        """
        try:
            repeat += 1
            self.get_element(locator).click()
            log.info(u'点击元素:%s' % locator)
        except Exception, e:
            log.info(u'点击元素:%s 第%s次执行失败' % (locator, repeat))
            if repeat > 2:
                raise e
            self.click(locator, repeat)

    def click_all(self, locator):
        """
        点击所有符合条件的元素
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        """
        allc = self.get_elements(locator)
        i = 0
        log.info(u'开始执行click_all,共%s个元素' % (len(allc)))
        for ele in allc:
            self.sleep(0.3)
            ele.click()
            i += 1
            log.info(u'点击第 %s 个元素' % i)

    def double_click_all(self, locator):
        """
        双击所有符合条件的元素
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        """
        allc = self.get_elements(locator)
        i = 0
        log.info(u'开始执行double_click_all,共%s个元素' % (len(allc)))
        for ele in allc:
            ActionChains(self.driver).double_click(ele).perform()
            i += 1
            log.info(u'点击第 %s 个元素' % i)

    def get_element_offset(self, locator):
        """
        获取元素坐标
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        :return: x,y
        """
        element = self.get_element(locator)
        loc = element.location
        x = loc['x']
        y = loc['y']
        log.info(u'获取元素坐标:%s,%s' % (x, y))
        return x, y

    def get_element_offset_click(self, locator):
        """
        获取元素坐标并点击中间位置,适用于:元素A中套着元素B,元素B无法定位但元素A可以定位
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        """
        element = self.get_element(locator)
        loc = element.location
        x = loc['x']
        y = loc['y']
        size = element.size
        width = size['width']
        height = size['height']
        x += width
        y += height
        self.click_offset(x, y)

    def click_offset(self, x, y):
        """
        点击坐标
        :param x: x坐标'
        :param y: y坐标'

        """
        ActionChains(self.driver).move_by_offset(x, y).click().perform()
        log.info(u'点击坐标%s,%s' % (x, y))

    def right_click(self, locator):
        """
        鼠标右击元素
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        """
        element = self.get_element(locator)
        ActionChains(self.driver).context_click(element).perform()
        log.info(u'在元素上右击:%s' % locator)

    def double_click(self, locator):
        """
        双击元素
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        """
        element = self.get_element(locator)
        ActionChains(self.driver).double_click(element).perform()
        log.info(u'在元素上双击:%s' % locator)

    def move_to_element(self, locator):
        """
        鼠标指向元素
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        """
        element = self.get_element(locator)
        ActionChains(self.driver).move_to_element(element).perform()
        log.info(u'指向元素%s' % locator)

    def drag_and_drop(self, locator, target_locator):
        """
        拖动一个元素到另一个元素位置
        :param locator: 要拖动元素的定位
        :param target_locator: 目标位置元素的定位
        """
        element = self.get_element(locator)
        target_element = self.get_element(target_locator)
        ActionChains(self.driver).drag_and_drop(element, target_element).perform()
        log.info(u'把元素 %s 拖至元素 %s' % (locator, target_locator))

    def drag_and_drop_by_offset(self, locator, xoffset, yoffset):
        """
        拖动一个元素移动x,y个偏移量
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        :param xoffset: X offset to move to
        :param yoffset: Y offset to move to
        """
        element = self.get_element(locator)
        ActionChains(self.driver).drag_and_drop_by_offset(element, xoffset, yoffset).perform()
        log.info(u'把元素 %s 拖至坐标:%s %s' % (locator, xoffset, yoffset))

    def click_partial_text_link(self, text):
        """
        按部分链接文字查找并点击链接
        :param text: 链接的部分文字
        """
        self.get_element('plink,' + text).click()
        log.info(u'点击连接:%s' % text)

    def alert_text(self):
        """
        返回alert文本
        :return: alert文本
        """
        log.info(u'获取弹框文本:%s' % self.driver.switch_to.alert.text)
        return self.driver.switch_to.alert.text

    def alert_accept(self):
        """
        alert点确认
        """
        self.driver.switch_to.alert.accept()
        log.info(u'点击弹框确认')

    def alert_dismiss(self):
        """
        alert点取消
        """
        self.driver.switch_to.alert.dismiss()
        log.info(u'点击弹框取消')

    def get_attribute(self, locator, attribute):
        """
        返回元素某属性的值
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        :param attribute: 属性名称
        :return: 属性值
        """
        value = self.get_element(locator).get_attribute(attribute)
        log.info(u'获取元素 %s 的属性值 %s 为:%s' % (locator, attribute, value))
        return value

    def get_ele_text(self, locator):
        """
        返回元素的文本
        :param locator: 定位方法+定位表达式组合字符串,用逗号分隔,如'css,.username'
        :return: 元素的文本
        """
        log.info(u'获取元素 %s 的文本为:%s' % (locator, self.get_element(locator).text))
        return self.get_element(locator).text

    def frame_in(self, locator):
        """
        进入frame
        :param locator: 定位方法+定位表达式组合字符串,如'css,.username'
        """
        e = self.get_element(locator)
        self.driver.switch_to.frame(e)
        log.info(u'进入frame:%s' % locator)

    def frame_out(self):
        """
        退出frame返回默认文档
        """
        self.driver.switch_to.default_content()
        log.info(u'退出frame返回默认文档')

    def open_new_window_by_locator(self, locator):
        """
        点击元素打开新窗口,并将句柄切换到新窗口
        :param locator: 定位方法+定位表达式组合字符串,如'css,.username'
        """
        self.get_element(locator).click()
        self.driver.switch_to.window(self.driver.window_handles[-1])
        log.info(u'点击元素 %s 打开新窗口' % locator)

        # old_handle = self.driver.current_window_handle
        # self.get_element(locator).click()
        # all_handles = self.driver.window_handles
        # for handle in all_handles:
        #     if handle != old_handle:
        #         self.driver.switch_to.window(handle)

    def open_new_window_by_element(self, element):
        """
        点击元素打开新窗口,并将句柄切换到新窗口
        :param element: 元素对象
        """
        element.click()
        self.driver.switch_to.window(self.driver.window_handles[-1])
        log.info(u'点击元素打开新窗口')

    def js(self, script):
        """
        执行JavaScript
        :param script:js语句
        """
        self.driver.execute_script(script)
        log.info(u'执行JS语句:%s' % script)

    def scroll_element(self, locator):
        """
        拖动滚动条至目标元素
        :param locator: 定位方法+定位表达式组合字符串,如'css,.username'
        """
        script = "return arguments[0].scrollIntoView();"
        element = self.get_element(locator)
        self.driver.execute_script(script, element)
        log.info(u'滚动至元素:%s' % locator)

    def scroll_top(self):
        """
        滚动至顶部
        """
        self.js("window.scrollTo(document.body.scrollHeight,0)")
        log.info(u'滚动至顶部')

    def scroll_bottom(self):
        """
        滚动至底部
        """
        self.js("window.scrollTo(0,document.body.scrollHeight)")
        log.info(u'滚动至底部')

    def back(self):
        """
        页面后退
        """
        self.driver.back()
        log.info(u'页面后退')

    def forward(self):
        """
        页面向前
        """
        self.driver.forward()
        log.info(u'页面向前')

    def wait_text(self, text, per=3, count=10):
        """
        判断给定文本是否在页面上
        :param text: 要判断的文本
        :param per: 每次判断间断时间
        :param count: 判断次数
        :return: 存在返回True,不存在返回False
        """
        for i in range(count):
            if text in self.driver.page_source:
                log.info(u'判断页面上有文本:%s 第%s次' % (text, i+1))
                return True
            self.sleep(per)
            log.info(u'判断页面上没有文本:%s 共%s次' % (text, i+1))
            return False

    def refresh(self):
        """
        刷新页面
        """
        self.driver.refresh()
        log.info(u'刷新页面')

    def screenshot(self, info='-'):
        """
        截图,起名为:文件名-方法名-注释
        :param info: 截图说明
        """
        catalog_name = cf.get_value('screenshot_path')  # 从全局变量取截图文件夹位置
        if not os.path.exists(catalog_name):
            os.makedirs(catalog_name)
        class_object = inspect.getmembers(inspect.stack()[1][0])[-3][1]['self']  # 获得测试类的object
        classname = str(class_object).split('.')[1].split(' ')[0]  # 获得测试类名称
        testcase_name = inspect.stack()[1][3]  # 获得测试方法名称
        filepath = catalog_name + classname + "@" + testcase_name + info + ".png"
        self.driver.get_screenshot_as_file(filepath)
        log.info(u'截图:%s.png' % info)

    def close(self):
        """
        关闭当前页
        """
        self.driver.close()
        self.driver.switch_to.window(self.driver.window_handles[0])
        log.info(u'关闭当前Tab')

    def sleep(self, sec):
        time.sleep(sec)
        log.info(u'等待%s秒' % sec)


if __name__ == '__main__':
    bp = BasePage()
    bp.open('http://www.baidu.com')

五.元素定义特别用法说明

1.本框架支持selenium所有的定位方法,为了提高编写速度,改进了定位元素的使用方法,此时把定位元素的方法名和方法值,是用逗号隔开的字符串

2.xpath定位:i_keyword = 'xpath,//input[@id="kw"]'     

3.id定位:b_search = 'id,su' 		

4.其他定位方法同上

5.使用上面代码中type()方法,是在输入框中输入文字,调用时输入type(i_keyword, "输入内容")type()中会调用get_element()方法,对输入的定位表达式进行解析,并且会等待元素一段时间,当元素出现时立即进行操作

6.self.type(self.i_keyword, "学好selenium")      

7.self.click(self.b_search)      

在这里插入图片描述

六.search_page.py

1.PO模式中封装的百度的搜索页,继承了上面的BasePage类;并且页面类中定义了各业务功能和各控件的表达式,并将页面类上定义的各种业务功能和控件操作封装为方法。这样如果有多个用例,调用了此页面类上封装的业务功能和控件的操作方法,将来更新维护只需要在此页面类中修改,就可以所有用例就都更新
from page_object.base_page import BasePage

class SearchPage(BasePage):
    def __init__(self, driver):
        self.driver = driver

    # i=输入框, l=链接, im=图片, t=文字控件, d=div, lab=label
    # 输入框输入星空物语后,自动跳转搜索出来的结果:第一条含_百度百科的搜索结果
    l_baike = 'xpath,//a[(. = "星空物语_百度百科")]'

    # 下一页
    b_next_page = 'link,下一页>'

    # 上一页
    b_up_page = 'xpath,//a[(. = "<上一页")]'

    # 点击搜索结果的百科
    def click_result(self):
        self.open_new_window_by_locator(self.l_baike)
        self.sleep(3)

    # 点击下一页
    def click_next_page(self):
        self.click(self.b_next_page)

    # 点击上一页
    def click_previous_page(self):
        self.click(self.b_up_page)

七.test_search.py

1.百度搜索页的测试用例

1.1.1个是搜索后点击首个搜索结果可打开

1.2.2个是搜索结果可翻页,用例中的具体各个业务功能的操作均是调用上面页面类中封装好的操作方法
import sys
reload(sys)
sys.setdefaultencoding('utf8')
from page_object.home_page import HomePage
from page_object.search_page import SearchPage
import pytest
import config.config as cf


class TestSearch():
    """
    pytest:
    测试文件以test_开头
    测试类以Test开头,并且不能带有__init__方法
    测试函数以test_开头
    断言使用assert
    """
    driver = cf.get_value('driver')  # 从全局变量取driver
    home_page = HomePage(driver)
    search_page = SearchPage(driver)

    def test_click_result(self):
        """搜索页-点击首个搜索结果"""
        try:
            self.home_page.open_homepage()
            self.home_page.input_keyword(u'星空物语')  # 输入关键字
            self.search_page.click_result()  # 点击星空物语_百度百科
            assert self.home_page.wait_text(u'电视剧《一起来看流星雨》片头曲')  # 验证页面打开
            self.home_page.screenshot(u'打开搜索结果')
            self.search_page.close()  # 关闭百科页面
        except Exception, e:
            self.home_page.screenshot(u'打开搜索结果失败')
            raise e

    def test_click_next_page(self):
        """搜索页-搜索翻页"""
        try:
            self.search_page.click_next_page()  # 点下一页
            assert self.home_page.wait_element(self.search_page.b_up_page)  # 上一页出现
            self.search_page.scroll_element(self.search_page.b_up_page)  # 滚到上一页
            self.home_page.screenshot(u'搜索翻页')
        except Exception, e:
            self.home_page.screenshot(u'搜索翻页失败')
            raise e

if __name__ == '__main__':
    pytest.main(['-v', '-s', 'test_search.py'])
    # pytest.main(['-v', '-s'])

八.conftest.py

1.conftest.py是pytest提供数据、操作共享的文件,其文件名是固定的,不可以修改

2.conftest.py文件所在目录必须存在__init__.py文件

3.其他文件不需要import导入conftest.py,用例会自动查找

4.所有同目录测试文件运行前都会执行conftest.py文件

5.conftest.py中加入报错截图的功能,如果有需要在用例前、后执行一些操作,也都可以写在这里

6.conftest.py文件名称不能更改

7.conftest.py文件必须和用例放在一层目录
import pytest
from py._xmlgen import html
import config.config as cf
import logging

log = logging.getLogger('szh.conftest')


@pytest.mark.hookwrapper
def pytest_runtest_makereport(item):
    """当测试失败的时候,自动截图,展示到html报告中"""
    pytest_html = item.config.pluginmanager.getplugin('html')
    outcome = yield
    report = outcome.get_result()
    extra = getattr(report, 'extra', [])

    if report.when == 'call' or report.when == "setup":
        xfail = hasattr(report, 'wasxfail')
        if (report.skipped and xfail) or (report.failed and not xfail):
            file_name = report.nodeid.replace("::", "_") + ".png"
            driver = cf.get_value('driver')  # 从全局变量取driver
            screen_img = driver.get_screenshot_as_base64()
            if file_name:
                html = '<div><img src="data:image/png;base64,%s" alt="screenshot" style="width:600px;height:300px;" ' \
                       'onclick="window.open(this.src)" align="right"/></div>' % screen_img
                extra.append(pytest_html.extras.html(html))
        report.extra = extra
        report.description = str(item.function.__doc__)#.decode('utf-8', 'ignore')  # 不解码转成Unicode,生成HTML会报错
        # report.nodeid = report.nodeid.encode("utf-8").decode("unicode_escape")


@pytest.mark.optionalhook
def pytest_html_results_table_header(cells):
    cells.insert(1, html.th('Description'))
    cells.pop()  # 删除报告最后一列links

@pytest.mark.optionalhook
def pytest_html_results_table_row(report, cells):
    cells.insert(1, html.td(report.description))
    cells.pop()  # 删除报告最后一列links

# @pytest.fixture(scope='function')
# def testcase():
#     log.info(u'\n--------------------用例开始--------------------')
#     yield
#     log.info(u'\n--------------------用例结束--------------------')

九.run.py

1.用来初始化的工作,运行测试,以及测试收尾

2.将driver的初始化放在这里,并将driver存入全局变量。这样浏览器只打开一次即可运行所有的测试。如果想每个用例都打开、关闭一次浏览器,那可以把定义driver的方法放在conftest.py中

3.get_args()是封装的命令行参数解析,方便集成Jenkins

4.get_args()命令行参数

4.1.目前只是定义了一个环境参数-e,可设置测试环境preview,线上环境product,如要在线上运行测试,启动脚本输入:python run.py -e product

4.2.根据实际需要添加更多参数,并在get_args()中进行修改

5.main()封装pytest的命令行执行模式
import pytest
import config.config as cf
from util.log import Logger
import argparse
from selenium import webdriver
from util.mail import send_mail

def get_args():
    """命令行参数解析"""
    parser = argparse.ArgumentParser(description=u'可选择参数:')
    parser.add_argument('-e', '--environment', choices=['preview', 'product'], default='preview', help=u'测试环境preview,线上环境product')
    args = parser.parse_args()
    if args.environment in ('pre', 'preview'):
        cf.set_value('environment', 'preview')
        cf.set_value('site', 'http://www.baidu.com/')
    elif args.environment in ('pro', 'product'):
        cf.set_value('environment', 'product')
        cf.set_value('site', 'https://www.baidu.com/')
    else:
        print u"请输入preview/product"
        exit()

def set_driver():
    """设置driver"""
    # 配置Chrome Driver
    chrome_options = webdriver.ChromeOptions()
    chrome_options.add_argument('--start-maximized')  # 浏览器最大化
    chrome_options.add_argument('--disable-infobars')  # 不提醒chrome正在受自动化软件控制
    prefs = {'download.default_directory': cf.get_value('download_path')}
    chrome_options.add_experimental_option('prefs', prefs)  # 设置默认下载路径
    # chrome_options.add_argument(r'--user-data-dir=D:\ChromeUserData')  # 设置用户文件夹,可免登陆
    driver = webdriver.Chrome('{}\\driver\\chromedriver.exe'.format(cf.get_value('root_path')), options=chrome_options)
    cf.set_value('driver', driver)

def main():
    """运行pytest命令启动测试"""
    pytest.main(['-v', '-s', 'test_case/', '--html=report/report.html', '--self-contained-html'])

if __name__ == '__main__':
    cf.init()  # 初始化全局变量
    get_args()  # 命令行参数解析
    log = Logger('szh')  # 初始化log配置
    set_driver()  # 初始化driver
    main()  # 运行pytest测试集
    cf.get_value('driver').quit()  # 关闭selenium driver

    # 先将util.mail文件send_mail()中的用户名、密码填写正确,再启用发送邮件功能!!!
    # send_mail(['22459496@qq.com'])  # 将报告发送至邮箱

在这里插入图片描述

十.GitHub代码地址

https://github.com/songzhenhua/selenium_ui_auto
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐