在第三节基础上多了下面5个层级(具体层级可参考下图)

components层:  组件层,放置UI自动化公共组件(比如selenium的操作类)以及页面组件脚本(比如多个页面脚本相同,可以用组件形式存储,避免重复工作)

config层:            配置层,管理系统配置

log层:                日志层,放置UI自动化运行的日志信息

page层:             页面层,放置UI自动化页面操作脚本

screenshots层:  截图层,放置UI自动化运行中捕获的截图

UI自动化框架搭建(四):完整UI自动化框架实现-冯金伟博客园

config、log、screenshots不难理解,主要解释下page层和components层怎么来方便我们UI自动化脚本编辑?

1、代码维护层级清晰:
page层: 维护UI脚本页面操作
components层: 维护UI脚本组件
testcases层: 维护测试用例
2、减少重复工作:
page层维护登录、目录层级选择这些公共页面操作,只需要编写一次
components层维护页面操作的公共组件(比如多层目录选择),只需要编写一次

接下来根据上节的test_aaa.py用例文件做下扩展

首先page层,针对 test_aaa.py进行页面操作类封装

aaa.py,具体代码如下

#coding:utf-8
from component.common.webdriver_base import WebDriverBase
import time
from utils.log_util import LogUtil

logger = LogUtil("aaa").get_logger()
class TestAaa(WebDriverBase):

    def login1(self):
        # 访问百度首页
        self.open_url(r"http://www.baidu.com")
        # self.driver.get(r"http://www.baidu.com")
        # 百度输入框输入
        self.loc_method("kw", "send_keys", method='id', text="懒勺")
        # self.driver.find_element_by_id("kw").send_keys("懒勺")
        # 点百度一下
        self.loc_method("su", "click", method='id')
        # self.driver.find_element_by_id("su").click()
        #等待时间只是为了让你可以看到目前效果,可以省略
        time.sleep(2)

    def login2(self):
        # 访问qq首页
        self.open_url(r"http://www.qq.com")
        # self.driver.get(r"http://www.qq.com")
        # 点新闻链接
        self.loc_method("//a[text()='新闻']", "click", method='xpath')
        # self.driver.find_element_by_xpath("//a[text()='新闻']").click()
        # 等待时间只是为了让你可以看到目前效果,可以省略
        time.sleep(3)
        logger.info("测试login2方法")

test_aaa.py代码变更如下(为什么要把页面操作放到page层?分层方便代码维护,以及2个test类共用了相同的页面操作,可以直接调用,不需要重复维护):

# -*- coding:utf-8 -*-
import unittest
from page.aaa import TestAaa
import time

#QingQing类的名字任意命名,但命名()里的unittest.TestCase就是去继承这个类,类的作用就是可以使runner.run识别
class QingQing(unittest.TestCase):
    #unittest.TestCase类定义的setUpClass和tearDownClass方法前一定要加@classmethod,
    #setUpClass在这个类里面是第一个执行的方法
    #tearDownClass在这个类里面是最后一个执行的方法
    #中间的执行顺序是通过字符的大小进行顺序执行,命名必须test_开头

    #打开浏览器,获取配置
    @classmethod
    def setUpClass(self):
        self.aaa = TestAaa()

    def test_01_search_baidu(self):
        # 访问百度首页
        # 百度输入框输入
        # 点百度一下
        self.aaa.login1()

    #执行商品收费功能
    def test_02_search_qq_news(self):
        # 访问qq首页
        # 点新闻链接
        self.aaa.login2()

    #退出浏览器
    @classmethod
    def tearDownClass(self):
        self.aaa.quit_browser()

if __name__ ==  "__main__":
    unittest.main()

最后components层,对selenium做如下封装(为什么要封装,比如你点击和输入文本操作,一般前提还得考虑元素是否存在才能去点击或输入,这部分重复性工作可以省去)

封装类webdriver_base.py,具体代码如下

# -*- coding:utf-8 -*-
from time import sleep

import os
from selenium.common.exceptions import *
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium import webdriver
from utils.datetime_util import DateTimeUtil

from utils.log_util import LogUtil
from utils.yaml_util import YamlUtil

logger = LogUtil('webdriver_base').get_logger()

driver = None
class WebDriverBase(object):
# 页面操作基础类

    def __init__(self):
        global driver
        # 如果driver不为空,直接使用原来的driver
        if driver !=  None:
            self.driver = driver
            return

        # 获取驱动
        chromeDriverPath = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'driver',
                                             'chromedriver.exe')
        option = webdriver.ChromeOptions()
        option.add_argument("disable-infobars")
        # 获取配置文件
        sysConfig = YamlUtil('sysconfig.yaml').read_yaml()
        # 找浏览器的名字
        browserName = sysConfig['browser']['browserName']
        if str(browserName).lower() == 'chrome':
            # 获取谷歌的驱动
            driver = webdriver.Chrome(executable_path=chromeDriverPath, chrome_options=option)
            self.driver = driver
        else:
            logger.error("暂不支持谷歌以外的驱动")
            raise Exception("暂不支持谷歌以外的驱动")

        if self.driver == None:
            logger.error("打开浏览器驱动失败")
            raise Exception("打开浏览器驱动失败")

        self.maximize_window()

    def open_url(self, url):
        # 访问浏览器地址
        self.driver.get(url)

    def get_driver(self):
        return self.driver

    def loc_method(self, eleLoc, action, method='CSS', text=None):
        """
        通用元素定位方法主入口
        :param eleLoc: 定位的元素路径
        :param action: 页面动作(输入文本,点击等等)
        :param method: 定位方式(css, path)提示:id、name、class属性都可以用css定位到,默认为CSS
        :param text: 如果是需要文本信息输入校验,才需要用到
        :return:
        """

        #loc放到selenium的driver.find_element方法就会自动识别元素
        if str(method).upper() == 'CSS':
            loc = (By.CSS_SELECTOR, eleLoc)
        elif str(method).upper() == 'XPATH':
            loc = (By.XPATH, eleLoc)
        elif str(method).upper() == 'ID':
            loc = (By.ID, eleLoc)
        elif str(method).upper() == 'NAME':
            loc = (By.NAME, eleLoc)
        elif str(method).upper() == 'CLASS':
            loc = (By.CLASS_NAME, eleLoc)
        else:
            loc = None

        try:
            if loc != None:
                if action == 'click':
                    self.click(loc)
                elif action == 'send_keys':
                    self.send_keys(text, loc)
                elif action == 'select_by_text':
                    self.select_by_text(text, loc)
                elif action == 'select_by_index':
                    self.select_by_index(text, loc)
                elif action == 'select_by_value':
                    self.select_by_value(text, loc)
                elif action == 'get_element_text':
                    return self.get_element_text(loc)
                elif action == 'get_element_attribute':
                    return self.get_element_attribute(text, loc)
                elif action == 'text_in_element':
                    return self.text_in_element(text, loc)
                elif action == 'value_in_element':
                    return self.value_in_element(text, loc)
                else:
                    logger.error("action错误:请确认action值:%s" % action)
            else:
                logger.error("method错误:请确认method值:%s" % method)
        except Exception as e:
            logger.error(e)

    def send_keys(self, text, loc):
        # 输入框输入文本信息,先清除文本框内容后输入
        self.clear_input_box(loc)
        try:
            self.find_element(*loc).send_keys(text)
            sleep(1)
        except Exception as e:
            logger.error(e)
            self.get_screen_img()
            raise

    def clear_input_box(self, loc):
        # 清除输入框内容
        self.find_element(*loc).clear()
        sleep(1)

    def click(self, loc):
        # 点击
        try:
            self.find_element(*loc).click()
            sleep(2)
        except Exception as e:
            logger.error(e)
            self.get_screen_img()
            raise

    def move_to_element(self, *loc):
        # 鼠标悬停
        above = self.find_element(*loc)
        ActionChains(self.driver).move_to_element(above).perform()

    def close_single_window(self):
        # 关闭当前窗口(单个的)
        self.driver.close()

    def quit_browser(self):
        # 退出浏览器,关闭所有窗口
        self.driver.quit()

    def maximize_window(self):
        # 浏览器窗口最大化
        self.driver.maximize_window()

    def browser_forward(self):
        # 浏览器前进
        self.driver.forward()

    def browser_back(self):
        # 浏览器后退
        self.driver.back()

    def browser_refresh(self):
        # 浏览器刷新
        self.driver.refresh()

    def get_element_text(self, loc):
        # 获取元素的文本
        return self.find_element(*loc).text

    def get_element_attribute(self, attributeItem, loc):
        # 获取元素的属性,可以是id,name,type或其他任意属性
        return self.find_element(*loc).get_attribute(attributeItem)

    def implicitly_wait(self, seconds):
        # 隐式等待时间,最长等待seconds秒,超过抛出超时异常,常用于页面加载等待
        self.driver.implicitly_wait(seconds)

    def select_by_index(self, index, *loc):
        # 通过index 下标取select
        ele = self.find_element(*loc)
        Select(ele).select_by_index(index)
        sleep(1)

    def select_by_value(self, value, *loc):
        # 通过value值取select
        ele = self.find_element(*loc)
        Select(ele).select_by_value(value)
        sleep(1)

    def select_by_text(self, text, loc):
        # 通过文本text值取select
        ele = self.find_element(*loc)
        Select(ele).select_by_visible_text(text)
        sleep(1)

    def text_in_element(self, text, *loc, timeout=10):
        # 判断某个元素的text是否包含了预期的值
        # 没定位到元素返回False,定位到元素返回判断结果布尔值true
        try:
            ele = WebDriverWait(self.driver, timeout, 1).until(EC.text_to_be_present_in_element(*loc, text))
        except TimeoutException:
            logger.error("查找超时,%s不在元素的文本里面" % text)
            return False
        return ele

    def value_in_element(self, value, *loc, timeout=10):
        # 判断某个元素的value是否包含了预期的值
        # 没定位到元素返回False,定位到元素返回判断结果布尔值true
        try:
            ele = WebDriverWait(self.driver, timeout, 1).until(EC.text_to_be_present_in_element_value(*loc, value))
        except TimeoutException:
            logger.info("查找超时,%s不在元素的value里面" % value)
            return False
        return ele

    def find_element(self, *loc):
        """
        定位元素
        :param loc: 元组 示例:(By.CSS,'id')
        :return:
        """
        try:
            WebDriverWait(self.driver, 10).until(lambda driver: driver.find_element(*loc).is_displayed())
            return self.driver.find_element(*loc)
        except NoSuchElementException:
            logger.error("找不到定位的元素:%s" % loc[1])
            raise
        except TimeoutException:
            logger.error("元素查找超时:%s" % loc[1])
            raise

    def get_screen_img(self):
        #截图保存ui运行结果
        imgPath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'screenshots')
        screenName = DateTimeUtil().get_current_time() + '.png'
        screenFile = os.path.join(imgPath, screenName)
        try:
            self.driver.get_screenshot_as_file(screenFile)
        except Exception as e:
            logger.error("没有成功截到图,原因是: %s" % e)

    def switch_to_next_window(self, currentHandle):
        # 当打开的窗口不是当前窗口,就切换
        allHandles = self.driver.window_handles
        for handle in allHandles:
            if handle != currentHandle:
                self.driver.switch_to.window(handle)
                break

    def switch_to_next_frame(self, iframe):
        # 表单切换到iframe,其中iframe是id
        self.driver.switch_to.frame(iframe)

    def execute_script(self, js):
        #执行js命令
        self.driver.execute_script(js)

View Code

截图中提到的工具类和配置代码如下

log_util.py

# -*- coding:utf-8 -*-
import logging
from datetime import datetime
import os


class LogUtil():
    def __init__(self, logname=None):
        # 日志名称
        self.logger = logging.getLogger(logname)
        # 日志级别
        self.logger.setLevel(logging.DEBUG)
        # 日志输出到控制台
        self.console = logging.StreamHandler()
        self.console.setLevel(logging.DEBUG)
        # 输出到文件
        self.date = datetime.now().strftime("%Y-%m-%d") + '.log'
        self.filename = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs', self.date)
        self.file = logging.FileHandler(self.filename, encoding='utf-8')
        self.file.setLevel(logging.DEBUG)
        # 日志显示内容
        self.formatstr = '%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s %(message)s'
        self.format = logging.Formatter(self.formatstr)
        self.console.setFormatter(self.format)
        self.file.setFormatter(self.format)
        # 加入到hander
        self.logger.addHandler(self.console)
        self.logger.addHandler(self.file)

    def get_logger(self):
        return self.logger

View Code

datebase_util.py

# -*- coding:utf-8 -*-
from utils.log_util import LogUtil
from utils.yaml_util import YamlUtil
import pymysql
import cx_Oracle

logger = LogUtil('database_util').getLogger()

class DataBase(object):
    def __init__(self):
        pass

    def queryDataBase(self, querySql):
        # 获取游标
        try:
            cursor = self.con.cursor()
            cursor.execute(querySql)
            return cursor.fetchone()[0]
        except Exception as e:
            logger.error(e)
        finally:
            self.con.close()

    def updateData(self, querySql):
        # 修改数据库数据
        try:
            cursor = self.con.cursor()
            cursor.execute(querySql)
            self.con.commit()
        except Exception as e:
            self.con.rollback()
            logger.error(e)
        finally:
            self.con.close()


class OracleDataBase(DataBase):
    def __init__(self):
        sysConfig = YamlUtil('sysconfig.yaml').readYaml()
        host = sysConfig['oralceConfig']['host']
        port = sysConfig['oralceConfig']['port']
        user = sysConfig['oralceConfig']['username']
        pwd = sysConfig['oralceConfig']['password']
        database = sysConfig['oralceConfig']['database']
        self.con = cx_Oracle.connect("{}/{}@{}:{}/{}".format(user, pwd, host, port, 
                                                             database).format(), encoding="UTF-8", nencoding="UTF-8")

class MysqlDataBase(DataBase):
    def __init__(self):
        sysConfig = YamlUtil('sysconfig.yaml').readYaml()
        host = sysConfig['mysqlConfig']['host']
        port = sysConfig['mysqlConfig']['port']
        user = sysConfig['mysqlConfig']['username']
        pwd = sysConfig['mysqlConfig']['password']
        database = sysConfig['mysqlConfig']['database']
        self.con = pymysql.Connect(
            host=host,
            port=port,
            user=user,
            passwd=pwd,
            db=database,
            charset='utf8'
        )

if __name__ == "__main__":
    pass

View Code

yaml_util.py

# -*- coding:utf-8 -*-
import os

from ruamel import yaml

from utils.log_util import LogUtil

logger = LogUtil('yaml_util').get_logger()


class YamlUtil(object):
    def __init__(self, file=None):
        try:
            if file != None:
                self.configPath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
                                               'config', file)
            if self.configPath:
                with open(self.configPath, 'r', encoding='utf-8') as f:
                    self.Yamlobject = yaml.safe_load(f)
        except Exception as e:
            logger.error(e)

    def read_yaml(self):
        return self.Yamlobject

    def write_yaml(self, name, value):
        self.Yamlobject[name] = value
        with open(self.file, 'w+', encoding='utf-8') as  fout:
            yaml.dump(self.Yamlobject, fout, default_flow_style=False, allow_unicode=True)


if __name__ == '__main__':
    configPath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
                              'config', 'sysconfig.yaml')
    r = YamlUtil(configPath).read_yaml()
    print(r['browser']['browserName'])

View Code

datetime_util.py

# -*- coding:utf-8 -*-
from datetime import datetime


class DateTimeUtil(object):
    def __init__(self):
        pass

    def get_current_time(self):
        return datetime.now().strftime("%Y%m%d%H%M%S")

    def get_current_date(self):
        return datetime.now().strftime("%Y-%m-%d")

if __name__=="__main__":
    dateTime = DateTimeUtil()
    print(dateTime.get_current_time())

View Code

sysconfig.yaml

browser:
    browserName:  chrome

login:
    account: renlk24211
    passwd: '12345678'
    url: https://blade.com.cn

mysqlConfig:
    host: 192.168.160.141
    port: 3306
    username: root
    password: 123456
    database: auto

oracleConfig:
    host: 192.168.160.141
    port: 3306
    username: root
    password: 123456
    database: auto

db2Config:
    host: 192.168.160.141
    port: 3306
    username: root
    password: 123456
    database: auto

View Code

主入口run_all_case.py封装

# -*- coding:utf-8 -*-
import unittest
import os
from utils.HTMLTestRunnerForPy3 import HTMLTestRunner
from datetime import datetime


class RunAllCase(object):

    def __init__(self):
        pass

    def add_cases(self):
        # 挑选用例,pattern='test_*.py'表示添加test_开头的py文件
        casePath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testcases')
        discover = unittest.defaultTestLoader.discover(
            start_dir=casePath,
            pattern='test_*.py'
        )

        return discover

    def get_report_file_path(self):
        # 指定生成报告地址
        report_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'reports')
        report_name = datetime.now().strftime("%Y%m%d%H%M%S") + '.html'
        report_file = os.path.join(report_path, report_name)

        return report_file

    def run_cases(self, report_file, discover, title, description):
        # 运行用例
        runner = HTMLTestRunner(
            stream=open(report_file, 'wb'),
            # 生成的html报告标题
            title=title,
            # 1是粗略的报告,2是详细的报告
            verbosity=2,
            # 生成的html描述
            description=description
        )
        runner.run(discover)

if __name__ == "__main__":
    r = RunAllCase()
    discover = r.add_cases()
    report_file = r.get_report_file_path()

    title = '银行UI自动化测试报告'
    description = '银行UI自动化测试报告'
    r.run_cases(report_file, discover, title, description)

View Code