全栈测试开发----unittest的设计及实现----自动化测试分层思想(1)

发布时间 2023-07-12 21:29:58作者: C_N_Candy

  通过unittest框架完成自动化分层操作,实现数据分离,减少代码于数据之间的依赖性,完成报告的生成并自动发送一系列操作。

 

前言:

  有人认为,在进行自动化测试过程中,测试代码只需要包含测试逻辑即可。其实不然,他需要包括很多类的代码,如URL拼接、访问UI控件、HTML/XML的解析等,如果将测试逻辑代码于上面这些类型的代码混合在一起,那么代码就显得非常难以理解,也不容易维护,所以需要使用分层结构来解决自动化测试过程中遇到的这些问题。

 

正文:

 

 一、为什么要写框架

   首先需要了解什么 是框架,。框架用于解决或者处理复杂的问题并将其简单化,从个人方面来说,如果在自动化测试过程中融入了框架,可以提高脚本的规范性,这也是面试的加分项,是自身能力的硬性指标。从实际工作而言,一个好的测试框架可以提升项目的稳定性、健壮性。降低维护成本,也非常容易解决问题,如准确定位问题等。

 

 二、自动化技术

   现如今主要的软件自动化测试技术有录制/回放、脚本技术、数据驱动、关键字驱动、业务驱动。

  在学习数据驱动之前,先了解脚本技术的发展,脚本技术最早是基于线性脚本发展的,所谓线性就是一行一行的顺序执行代码。该阶段只适用于让初学者理解selenium相关API接口的操作,该类型脚本在实际工作中你那个没有任何意义。

  线性脚本之后就是结构化脚本。主要是通过使用selenium+API+Python模块封装面向对象(类于对象)的脚本,该类型脚本主要包括两种类型:模块化脚本、库/包脚本,不同的业务场景会涉及不同的模块。

  随着结构化脚本的发展,慢慢的进入数据驱动时代。数据驱动时代实现了将脚本中的数据于代码进行分离的操作,从而减少了其彼此的依赖,脚本的利用率、可维护性大大提高,但是界面变化对其影响仍然非常大。

  基于这种考虑,数据驱动技术将测试逻辑按照关键字进行分解,形成数据文件,关键字对应封装的业务逻辑,即关键字驱动,这也是数据驱动测试的一种改进。目前大多数测试工具技术处于数据驱动技术到关键字驱动技术之间。

  最后就是根据关键字驱动完成不同类型的业务驱动,业务驱动可以分为接入层业务驱动、业务层业务驱动、数据层业务驱动和性能驱动等。

发展:线性脚本——>结构化脚本——>数据驱动——>关键字驱动

 

三、如何分层封装

  根据不同的设计者、公司定义规则,参考主流分层的架构模式等所得到的分层模型会略有不同,但是整体的核心分层思想不变。

  一般将分层层次定义为4~6层,可以根据实际情况进行封装设计。

    1、页面元素处理层:即Page Object(PO模式)表页面对象管理,将每个页面上的所有元素定义在一个模块中,即使后期要对前段页面进行修改,其脚本的维护也十分方便。

    2、业务流操作层:表示基于页面元素处理层实现业务流的自由组织,实际对应自动化测试的业务流场景的执行测试用例

    3、测试用例层:根据业务流场景设计相应的测试用例并执行,用例的执行都是通过框架完成(unittest、pytest),并且可以很好的自由组织测试用例的执行并分析结果。

    4、数据分离层:将脚本中的所有数据提取出来进行专门的数据模块管理,后期直接修改相应的数据即可,不需要进行底层代码的查看分析。

    5、公共层:进行常量数据的存储、报告的生成、日志的保存、邮件的发送等

    6、主程序入口应用层:执行只需要设定一个入口,最后整体框架只需要执行主程序入口模块、修改数据分离层中的数据、新增测试用例层的用例即可,其他底层进行封装。

 

四、驱动器对象示例封装

  首先需要封装驱动器对象的获取方法,可以将驱动器对象中的浏览器进行基本设置,如驱动器文件、浏览器下载默认地址、传入不同浏览器参数完成不同浏览器驱动器对象创建等。再着就是元素定位的方式,前面总结的定位方式有三十多种,其中常规八种、复数八种、By形式八种、JS方式六种、jQuery,还有XPath模糊定位、二次定位等,这些都可以直接封装到方法中

 

# -----------------------------------
'''
@Author:C_N_Candy
@Date  :2023/7/9 17:07
@File  :GET_PATH.py
@Desc  :获取当前工作目录路径
'''
# ------------------------------------
import os


def GETPATH():
    path = os.getcwd()  # 获取当前工作目录
    # print(path)
    return path


if __name__ == '__main__':
    GETPATH()
# -----------------------------------
'''
@Author:C_N_Candy
@Date  :2023/7/9 18:01
@File  :Base_Page.py
@Desc  :从所有需要获取驱动器对象的页面抽取出来的基类
'''
# ------------------------------------
from selenium import webdriver
import os
import subprocess
import sys
from selenium.common.exceptions import WebDriverException
from CRMProject.GET_PATH import GETPATH
from selenium.webdriver.common.by import By

# 设定驱动器版本参数,当前自带的驱动器Chrome:70以上当前的火狐:80以上
broser_version = {'Chrome': 70, 'FireFox': 80, 'Edge': 18}


# 此模块相当于封装底层所有相关内容:驱动器对象、元素定位方式
class BasePage(object):
    def __init__(self, url, browsertype="Chrome"):
        """

        :param url: 需要访问的URL
        :param browsertype: 传入浏览器类型实现对应的浏览器驱动器对象创建:忽略大小写;默认使用谷歌浏览器;其值可以是Firefox、IE、Edge
        """
        __browsertype = browsertype.lower()  # 忽略大小写,该变量不能被外界直接访问,将其设置为私有变量
        self.download_path = GETPATH() + "Page_Object\Base\DownLoad"  # 所有资源的下载储存目录
        __driverdir = GETPATH() + "Page_Object\Base\DriverDir"  # 驱动器地址只需要当前模块引用,其他模块不需要引用
        try:
            if __browsertype == "FireFox":
                fireoptions = webdriver.FirefoxProfile()
                # 设定下载方式,2表示保存到指定的文件夹路径
                fireoptions.set_preference("broser.download.folderList", 2)
                # 设定文件夹路径
                fireoptions.set_preference("broser.download.dir", self.download_path)
                """
                去除询问窗口,定义下载文件的文本类型:content-type:text/html text/css text/json
                """
                fireoptions.set_preference("broser.helperApps.neverAsk.saveToDisk", "application/octet-stream")
                self.driver = webdriver.Firefox(firefox_profile=fireoptions,
                                                executable_path=__driverdir + "\geckodriver.exe")

            elif __browsertype == "Chrome":
                options = webdriver.ChromeOptions()
                # options的值都以键值对的形式存在:设置次数最多的就是修改下载的默认路径
                dict1 = {"download.default_directory": self.download_path}
                # 将设定的操作添加到谷歌浏览器中:prefs是谷歌浏览器存在一个选项的关键字名称,不能随意定义
                options.add_experimental_option("prefs", dict1)
                self.driver = webdriver.Chrome(options=options, executable_path=__driverdir + "\chromedriver.exe")

            elif __browsertype == "Ie":
                self.driver = webdriver.Ie()

            elif __browsertype == "Edge":
                # 确定Edge的驱动已经安装的话,
                # os.system("DISM.exe /Online /Add-Capability /CapabilityName:Microsoft.WebDriver~~~~0.0.1.0")
                # self.driver = webdriver.Edge()
                self.__check_edge_driver()
                # 以上都错误的话,则可以抛出驱动器异常,或者print输出错误语句,实际上后期优化,写入日志中
            else:
                print("传入的浏览器类型参数错误")

            self.driver.get(url)
            self.driver.maximize_window()
            self.driver.implicitly_wait(30)

        except:
            print("%s驱动器与当前浏览器不匹配!" % __browsertype)

    # 声明一个方法判定Edge的驱动器是否下载
    def __install_edge_driver(self, username="administrator", password="123456"):
        sub2 = subprocess.run(
            'Isrunase/user:%s/password:%s/domain:/command:"DISM.exe /Online /Add-Capability /CapabilityName:Microsoft.WebDriver~~~~0.0.1.0"/runpath:c:\\' % (
                username, password), capture_output=True)
        # 返回的是输出结果。如果命令执行成功是没有结果输出的,返回码是0
        return (sub2.stdout, sub2.returncode)

    # 实现Edge驱动器是否安装的判定
    def __check_edge_driver(self):
        try:
            self.driver = webdriver.Edge()
        except WebDriverException:
            get_result = self.__install_edge_driver()
            if len(get_result[0]) == 0 and get_result[1] == 0:
                self.driver = webdriver.Edge()
            else:
                print("当前驱动器版本与浏览器版本不一致")
        except Exception:
            print(sys.exc_info())

    @staticmethod
    def get_element1(driver, type, value, js_type='id'):  # 默认id;忽略大小写
        __get_type = type.lower()
        if __get_type == 'id':
            return driver.find_element(By.ID, value)

    # 封装所有定位方式的方法
    def get_element(self, type, value, js_type="id"):
        """

        :param type: 其值可以是id,class,name,tag,link,partial,XPath,CSS,JS,jQuery;如果不是指定类型的参数,则会抛出异常
        :param value:
        :param js_type:
        :return:
        """
        try:
            __get_type = type.lower()
            if __get_type == "id":
                return self.driver.find_element(By.ID, value)
            elif __get_type == "class":
                return self.driver.find_element(By.CLASS_NAME, value)
            elif __get_type == "name":
                return self.driver.find_element(By.NAME, value)
            elif __get_type == "tag":
                return self.driver.find_element(By.TAG_NAME, value)
            elif __get_type == "link":
                return self.driver.find_element(By.LINK_TEXT, value)
            elif __get_type == "XPath":
                return self.driver.find_element(By.XPATH, value)
            elif __get_type == "CSS":
                return self.driver.find_element(By.CSS_SELECTOR, value)
            elif __get_type == "partial":
                return self.driver.find_element(By.PARTIAL_LINK_TEXT, value)
            elif __get_type == "JS":
                return self.js_element(value, js_type)
            elif __get_type == "jQuery":
                pass
            else:
                raise ValueError
        except:
            print(sys.exc_info())

    # JS定位封装
    def js_element(self,value, js_type='id'):
        __js_type=js_type.lower()

        if __js_type=="id":
            return self.driver.execute_script("return document.getElementById('%s')"%value)
        # 如果是name,tag,css返回多个元素对象,默认操作第一个
        elif __js_type=="name":
            return self.driver.execute_script("return document.getElementByName('%s')"%value)[0]
        elif __js_type=="tag":
            return self.driver.execute_script("return document.getElementByTagName('%s')"%value)[0]
        elif __js_type=="CSS":
            return self.driver.execute_script("return document.querySelectorAll('%s')"%value)[0]
        elif __js_type=="class":
            return self.driver.execute_script("return document.getElementByClassName('%s')"%value)
        else:
            raise ValueError


    def get_elements(self,type,value):
        """

        :param type: 其值可以是id,class,name,tag,link,partial,XPath,CSS,JS,jQuery
        :param value:
        :return:
        """
        __get_type = type.lower()
        if __get_type == "id":
            return self.driver.find_elements(By.ID, value)
        elif __get_type == "class":
            return self.driver.find_elements(By.CLASS_NAME, value)
        elif __get_type == "name":
            return self.driver.find_elements(By.NAME, value)
        elif __get_type == "tag":
            return self.driver.find_elements(By.TAG_NAME, value)
        elif __get_type == "link":
            return self.driver.find_elements(By.LINK_TEXT, value)
        elif __get_type == "XPath":
            return self.driver.find_elements(By.XPATH, value)
        elif __get_type == "CSS":
            return self.driver.find_elements(By.CSS_SELECTOR, value)
        elif __get_type == "partial":
            return self.driver.find_elements(By.PARTIAL_LINK_TEXT, value)
        elif __get_type == "jQuery":
            pass
        else:
            raise ValueError

 

 对驱动器对象、各种定位方式封装。源码参考

 

 

参考扩展:

Python 中 OS 模块获取文件/目录路径方法 - 守护往昔 - 博客园 (cnblogs.com)

python3中super()参数意义和用法_super里面的参数_wowocpp的博客-CSDN博客

【Python】python之subprocess模块详解_python subprocess_伐尘的博客-CSDN博客

Python os 模块详解 - 知乎 (zhihu.com)

import os # 导入必要模块
 
main_path="I:/s/ss/"#文件保存路径,如果不存在就会被重建
if  not os.path.exists(main_path):#如果路径不存在
    os.makedirs(main_path)
import os
filename = '/Users/joseph/work/python/poem.txt'

if not os.path.exists(filename):
    os.system(r"touch {}".format(path))#调用系统命令行来创建文件