自动化框架设计及落地的那些事

前言

近期做了一个web UI 自动化的项目,框架用的是Python+PyTest+Selenium+BDD。以这个框架为学习的入口,整理出这份文档,想给大家诠释出项目在做UI自动化时的一些实操流程,目的是想给那些想学习自动化框架搭建的同学一些思考方向,当然在实际项目中我们或许会面对更为复杂的场景,并不止我所讲到的这些,欢迎大家一起交流。

如果你也不曾接触这个框架,想要练练手,建议可以先:

  • 了解一下Python 基础,明白类与对象,封装与继承
  • 了解 PyTest 以及 PyTest-BDD框架的运行机制
  • 了解Selenium基础知识,比如元素定位,元素等待,基本操作方法等

1 测试框架选型
1.1 对框架的需求分析

在项目中当我们的产品趋于稳定进入迭代优化的时候,每次发版前可能会伴随着越来越多的不会频繁变动的需求需要做回归测试,这个时候我们会考虑引入自动化。通常项目对UI自动化框架的需求分析是基于非常了解当前项目的情况下进行的,包括了解项目里自动化的人员能力情况,投入的资源情况,项目本身的需求,被测系统的实现等等。对框架的需求分析可能包含但也不限于以下几种,因项目而异:

  • 能支持项目所需的浏览器、系统
  • 代码复用率高
  • 测试业务与数据配置等解藕,降低维护成本。比如元素发生变化时,只需要更新一下yml配置文件即可
  • 有日志追踪,可快速的定位问题
  • 运行时的稳定性
  • 测试用例集能灵活的选取,如冒烟测试用例集,回归测试用例集等
  • 支持扩展,如发邮件等
  • 有的项目可能还需支持浏览器多tab页签
  • 支持切换环境
  • 可以批量运行用例并生成测试报告
  • CI/CD
  • 项目测试人员能力层次不同,对框架的上手难易程度有要求
1.2 测试方案确定

结合自身项目对UI自动化测试框架的需求的分析,自动化测试方案的确定我们可以从三个方面入手:控制方案、执行方案、结果上报方案,同时还应该考虑这三部分所普适的编程语言,从这些方面入手去找到适合自己项目需求的框架或工具,想要了解更多可以参看另一篇文章:自动化框架和工具有哪些?开篇已经提到本项目的框架客户结合自身项目情况已经确定,编程语言是Python,下面简单描述下着手确定UI自动化测试方案的三个方面:

  • 控制方案

控制方案即是采用一种工具来实现控制客户端进行模拟人工操作,大致的操作流程就是打开被测项目的网页,定位到目标位置,进行点击,输入等操作。目前相关的工具有:Selenium、Cypress、Playwright、Puppeteer等,详细了解这些框架之间的差异,再结合项目自身对于UI自动化的需求进行选取。我们项目选用的是Selenium,很“古早”,项目选中它有一些自身考虑的因素,这里不做好坏的讨论。

  • 执行方案

自动化测试的执行方案的核心在于选取一套最优的自动化测试框架,测试框架我理解主要用于实现测试用例的组织和执行,以及测试结果的生成等,目前基于python的测试框架主要有Unittest、Pytest、Pytest-BDD、Nose、Robot Framework等,我们项目选用的是PyTest 测试框架并结合了PyTest-BDD。BDD(行为驱动开发)是一种软件开发方法论,使用Gherkin语言编写测试场景,例如”Given-When-Then”的语法结构,它将需求和测试用例结合起来,以更加清晰地表达需求和期望行为。

  • 结果上报方案

我们项目选用的是Allure,其生成的报告样式简洁美观,同时也支持中文。


2 测试框架设计
2.1 指导思想

测试框架选型确定之后,我们接下来考虑的是如何在框架里编排我们的自动化代码、测试数据及配置等。为什么要做编排?这里有什么难点?又有什么办法可以指导解决的呢?带着这些疑问,我们继续。

举个例子,如果我们想自动化实现一个简单的业务测试流:打开百度网站 -> 输入关键 ->点击 搜索 ->查看验证结果。我们把这个业务流涉及到的内容全部写进一个Python文件里,包含了打开网页,定位元素,操作,断言等,看起来其实也比较清晰,但是如果以这样写代码的方式运用到一个项目里,可能会有几十几百个页面需要测试,随着时间的迁移,测试套件将持续的增长,我们的测试脚本变得越来越臃肿庞大。那页面元素的任何改变都会让我们的脚本维护变得繁琐复杂,而且变得耗时易出错。那怎么解决呢? 在UI自动化测试中,引入了Page Object Model(POM):页面对象模式,POM能让我们的测试代码变得可读性更好,高可维护性,高复用性。为了进一步理解POM模式,我们先来看看非POM和POM结构对比图:

可以看到非POM结构将所有代码都写在一个类中,包括元素,操作方法,业务测试代码等,这样很不利于代码维护,代码存在冗余,UI页面元素发生变化需要修改很多地方。POM结构则是将测试业务与具体的元素和操作方法等解藕,这样即便UI页面元素变化,我们只需要维护修改元素即可,调用的测试方法可以不用修改。

以这种模式来思考,我们也还可以分得更细,比如将Selenium的基础API进行封装为基类,每一个页面的元素和操作方法可以封装为一个page类(可以继承基类),并且元素可以放到另外一个配置文件来管理,再通过业务流组织成用例。如果有某个页面的元素需要变更,那么就可以直接更改页面元素的配置文件即可,这样整个的代码维护成本也会缩减,总的来说POM的核心就是分层思想,主要的分层:

  • 对象库层:Base(基类),封装page 页面一些公共的方法,如查打开网页,找元素方法、点击元素方法、输入方法、获取文本方法、截图方法等
  • 操作层:page(页面对象),封装页面对元素的操作,一个页面封装成一个对象,将对象库层封装的基础方法和页面元素进行整合
  • 业务层:business(业务层),将一个或多个操作组合起来完成一个业务功能。比如百度搜索:需要打开百度网页、输入关键字、点击百度搜索三个操作
2.2 测试框架结构

基于上述指导思想,可能大家还处于理论理解阶段,到具体项目里如何下手?

以我们项目为例,以POM模型为指导思想,我们将:

  • Selenium方法(Selenium的基类,对Selenium的基础方法进行封装)
  • 页面元素 (把页面元素单独提取出来,放入一个yml文件中)
  • 页面对象(整合Selenium方法和页面元素)
  • 测试用例 (基于PyTest及PyTest-BDD对整合的页面对象进行测试用例编写)

以上四种维度对代码主体进行了拆分,加上pytest框架自身的配置文件以及一些测试数据等,我们测试框架的整体结构就大致出来了。

主要目录及文件说明:(如果想要练练手,需手动按照这个结构,建相关的文件及目录)


3 测试依赖安装

确定好框架和项目目录结构之后,我们可以开始着手安装了。项目使用的是Pipenv来安装和管理的,Pipenv是python官方推荐的虚拟环境管理工具,可以把它当作是virtualenv,pip,pyenv三者的集合工具。 它能够自动为项目创建和管理虚拟环境,不用再维护 requirement.txt 了,使用 Pipfile 和 Pipfile.lock 来代替:从 Pipfile 文件中添加或者删除包,同时生成 Pipfile.lock 文件来锁定安装包的版本和依赖信息,避免构建错误。具体安装方法可以参考:Pipenv: 新一代Python项目环境与依赖管理工具


4 详细设计

这一章节会先详细介绍主要用到的pytest框架的一些特性,配置文件,页面元素,页面对象,Selenium基类封装等,让大家能了解到整体框架各个模块之间的关联除了开发一些工具外,还离不开框架本身的一些特性支撑。

4.1 pytest框架特性

本小节简单介绍我们项目用到的部分pytest特性,想要了解得更深入或更多大家可以到官网看看。

  • 能自动收集测试用例

pytest命令运行测试用例时,框架就会自动发现当前目录下所有符合规则的py文件,我们也可以在项目根目录下的pytest.ini文件中自定义用例收集规则。因此在编写测试用例的过程中需要大家遵守这些规则,这样程序才能如期运转。

[pytest]
bdd_features_base_dir = features/
python_files = test_*.py
python_classes = Test*
python_functions = test_*
  • 灵活运行指定的测试用例,可以给用例打上各种各样的标签,如:冒烟测试,集成测试,回归测试等

pytest.ini文件中自定义一些标签,pytest框架也有自带的一些标签。可以在测试用例上方进行标记使用。

markers =
    smoke: smoke test
    integration: integration test

  • fixture夹具灵活管理环境,很轻松的就可以进行测试环境的初始化和清理

下面举两个例子来简明描述fixture在我们项目中使用到的部分方式。

例1:比如用例运行之前自动进行环境数据清理,清除日志文件、下载文件、截图目录等,可以在conftest.py文件中定义一个函数并标记为fixture且autouse为True,这样的话在每次运行之前会自动执行该函数内的动作。这里scope的值是作用域,可选选项有function,class,module,session。

@pytest.fixture(scope='session', autouse=True)
def setup():
    # 测试执行之前初始化文件目录temp, report, picture
    ini_file_dic(cm.temp_path)
    ini_file_dic(cm.report_path)
    ini_file_dic(cm.png_path)

例2: 比如用例运行之前需要获取浏览器driver,运行之后需要关闭浏览器driver。可以在conftest.py文件中定义一个函数用来setup获取driver,并用yield关键字呼唤teardown操作。当driver用完也就是整个会话结束前会执行yield语句后面的动作driver.quit()。

@pytest.fixture(scope='session')
def driver(get_runtime_config):
    with Pyse(get_runtime_config) as pyse_instance:
        driver = pyse_instance.get_driver(browser=get_runtime_config.browser)
        driver.implicitly_wait(10)
        yield driver
        driver.quit()

大家可能好奇,这里没有autouse参数,那什么时候或者怎么调用这个定义为fixture的driver函数呢?很简单,直接将fixture名字driver像参数一样传递给调用的函数就可以啦。这里还需说明的一点是用例脚本需要使用fixture函数时,无需导入conftest.py这个文件,框架会自动去查找的。

@pytest.fixture(scope='module')
def search_page(driver):
    return Search(driver)

pytest自带的钩子函数

钩子函数在pytest称之为Hook函数,是为了让用户更好的去扩展开发而预留的一些函数。而预留的这些函数,在整个测试执行的生命周期中特定的阶段会自动去调用执行。如下图:

我们可以在conftest.py中去引用和扩展这些钩子,比如我们用到部分钩子函数:pytest_addoption,用来添加运行时命令参数;pytest_runtest_makereport,用来获取运行的测试结果。

4.2 配置文件及读取方法

第二章节提到配置文件都存放在config目录下,根据内容的不同,分为多个配置文件来进行管理的,对于配置文件这块我们需要做的是如何实现调用或读取这些文件内容:

conf.py文件

控制项目所有目录配置信息都写在这个文件里,以下例举部分:

class ConfigManager(object):
    # 项目目录
    base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    # 页面元素目录
    element_path = os.path.join(base_dir, 'page_element')
cm = ConfigManager()

外部模块若想要获取某个路径,调用方式比较简单:

先引入模块:
from config.conf import cm
再调用,比如需要获取页面元素目录直接使用:
cm.element_path

conf.ini文件

被测系统的URL,账号,密码以及测试过程中用到的需要支持的一些可配置参数,比如收件箱,邮件开关等信息都可以放在这个文件里。

[browser]
browser = chrome

[HOST]
;被测系统url
HOST = https://baidu.com

[username]
username = testName

[password]
password = testPasswd

外部模块若想要读取该配置文件的内容,我们在utils目录下封装一个readconfig的方法,提供读取conf.ini文件的能力,这样调用就比较方便了:

class ReadConfig(object):
    """配置文件"""

    def __init__(self):
        self.config = configparser.RawConfigParser()  # 当有%的符号时请使用Raw读取
        self.config.read(cm.ini_file, encoding='utf-8')

    def _get(self, section, option):
        """获取"""
        return self.config.get(section, option)

    @property
    def host(self):
        return self._get(HOST, HOST)

ini = ReadConfig()
先引入read config模块
from utils.readconfig import ini
再调用,比如需要获取被测系统的host, 直接调用:
ini.host

temp_config.yml 文件

存放运行结果的信息,用于运行完成后发送测试结果邮件。获取运行的测试结果是用到的是上述提到的钩子(Hooks)方法pytest_runtest_makereport,获取到结果后写入到这个文件中保存。

ENV: dev
TestResult:
- passed
- passed
TestStrategy: smoke
4.3  日志管理

可以写一个logger方法实现对日志进行处理,可以将运行过程中的日志打印到控制台也会生成日志文件存放到logs目录下,便于问题跟踪。(网络上有很多方法,大家可以找一个合适的)

引入包
from lib.logger import log

调用
log.info("成功打开网页:%s" % url)

日志打印
[2024-01-16 10:32:36,825] [pyse.py|open] [line:121] INFO    : 成功打开网页:https://baidu.com
4.4 页面元素的管理和读取

页面元素的管理

页面元素我们是放在page_element目录下来管理的,每个页面建一个对应的yml文件。之所以带上了元素定位的类型比如id,xpath 等,是因为我们已经将find_element_by_id,find_element_by_xpath等获取元素的方法在章节二中提到的lib-pyse.py中进行统一封装了。

搜索输入框: xpath=>//*[@id="kw"]
搜索按钮: xpath=>//input[@type="submit"]
搜索结果: xpath=>//span[contains(text(), "百度为您找到以下结果")]

页面元素读取

在utils目录下写了个readelement.py,封装了读取yml文件的类Element,在页面对象读取元素时只需要:

class Element(object):
    """获取元素"""

    def __init__(self, name):
        self.file_name = '%s.yml' % name
        self.element_path = os.path.join(cm.element_path, self.file_name)
        if not os.path.exists(self.element_path):
            raise FileNotFoundError("%s 文件不存在!" % self.element_path)
        with open(self.element_path, encoding='utf-8') as f:
            self.data = yaml.safe_load(f)

    def __getitem__(self, item):
        """获取属性"""
        data = self.data.get(item)
        if data:
            return data
        raise ArithmeticError("{}中不存在关键字:{}".format(self.file_name, item))
先在page模块中申明一个获取元素的对象,比如搜索页面:
search = Element('search')
如果需要获取到yml文件中‘搜索输入框’的locator,可直接用:
search['搜索输入框']
4.5 Selenium基类封装

之所以对Selenium的基础方法进行封装,是因为在web自动化中,因网络等原因会有不稳定性,可能经常需要等待元素或者做一些异常处理,如果在写测试的每一个地方都加上显示等待又会使代码冗余,并且难以维护,所以把一些等待或者异常处理等封装到selenium的基本方法中,这样就很方便多次调用。对selenium基础封装我们是放到lib-pyse.py文件的Pyse类中,里面包含了基本的:获取浏览器driver、打开网页、获取元素、判断元素是否显示、截图、点击、清除、输入、获取元素文本、下拉框根据文本选择等等一系列的操作,基本都能满足日常项目需要。

在页面对象直接可以继承Selenium基类,这样就可以直接调用父类的方法:

比如Search这个页面对象,继承了Pyse基类,就可以直接使用基类中的open方法来打开页面:

class Search(Pyse):
    def __init__(self, page):
        super().__init__(page)

    def open_baidu_page(self):
        self.open(ini.host)
Pyse基类中的open方法:

def open(self, url):
    try:
        self.driver.get(url)
        self.driver.implicitly_wait(10)
    except TimeoutException:
        self.allure_png("打开浏览器超时")
        raise TimeoutException("打开%s超时请检查网络或网址服务器" % url)
    else:
        log.info("成功打开网页:%s" % url)
        return True
4.6 页面对象

页面对象我们是放在page目录下管理,不同页面建不同的模块文件,封装一些该页面的基本操作。例如百度搜索:打开百度网页、输入关键字、点击百度搜索按钮、一些校验方法等等

search = Element('search')
class Search(Pyse):
    def __init__(self, page):
        super().__init__(page)

    def open_baidu_page(self):
        self.open(ini.host)

    def input_key(self, content):
        self.type(search['搜索输入框'], content)

    def click_search_btn(self):
        self.click(search['搜索按钮'])

    def verify_search_result(self, element):
        self.get_display(search[element])
4.7 测试用例编写

了解了整个框架的关联及特性,现在我们开始着手写一下自动化用例。一个完整的自动化测试用例包含:

  • 测试准备(setup):测试准备步骤,比如启动浏览器,初始化文件目录数据
  • 测试步骤(test steps):核心测试步骤,比如百度搜索测试的打开网页,点击搜索等业务操作流程
  • 断言(assertions):用例执行完成后的期望结果
  • 测试清理(teardown):对执行测试造成的影响进行清理和还原,以免影响后续执行,比如关闭浏览器

前面在fixture特性中已经用样例提到了setup和teardown的实现机制,也以百度搜索为例详细讲解了页面元素,页面对象等,下面就不再赘述,直接继续以百度搜索为例编写测试用例,我们引入的是BDD:

第一步:features目录下新建百度搜索的feature文件,添加如下内容,这里将输入的关键字进行参数化了,会作为两个case执行两次:

Feature: 百度搜索
  @smoke
  Scenario Outline: 百度搜索
    Given 打开百度首页
    When 输入关键字<key_wd>进行搜索搜索
    Then 进入搜索结果页面
    Examples:
      | key_wd |
      | 自动化 |
第二步:step_definitions目录下新建test开头的百度搜索测试文件,并添加feature文件中每一句对应的实现函数:
from pytest_bdd import scenario, given, when, then

@scenario('../features/search.feature', '百度搜索')
def test_search_function():
    pass

@given("打开百度首页")
def open_baidu(search_page):
    search_page.open_baidu_page()

@when("输入关键字<key_wd>进行搜索搜索")
def search_by_key_word(search_page, key_wd):
    search_page.input_key(key_wd)
    search_page.click_search_btn()

@then("进入搜索结果页面q")
def search_result_page(search_page):
    search_page.verify_search_result('搜索结果')
    search_page.allure_png('搜索结果页面展示正常')

上面就是整个测试业务流,整合了页面对象中封装的方法并加上了一些断言和截图。从上面步骤中大家可以看到调用了一个名叫search_page的fixture,这个fixture具体内容如下,其实就是返回实例化的search页面对象,且在方法中调用了fixture driver并将driver传递给了Search对象,因此Search页面对象中的一些封装方法就可以使用到driver做一些浏览器操作了。这一顿操作下来,我们又解锁了PyTest框架的一个新的知识点:fixture是可以再调用其他的fixture的。

@pytest.fixture(scope='module')
def search_page(driver):
    return Search(driver)

5 测试执行及结果处理
5.1 执行测试并生成报告

我们已经了解到了整个框架的关联及测试用例的输出,下面我们进入执行阶段,并在执行完成之后生成测试报告。

第一步:运行之前,需要先在项目根目录下激活虚拟环境:
$ pipenv shell
第二步:使用pytest命令运行smoke标签的测试用例,测试结果会存放到temp目录。另外浏览器,测试环境等参数在钩子函数pytest_addoption中默认已经传入。
$ pytest -s --alluredir=temp -m smoke
第三步:生成测试结果报告到report目录下(这里用到的项目根目录下已经下载好的allure包)
$ allure-2.16.1/bin/allure generate temp -o report --clean
第四步:打开测试报告
$ allure-2.16.1/bin/allure open -h 127.0.0.1 -p 8883 ./report/
5.2 测试结果发送邮件

测试执行结束后通常会将测试结果发送邮件给相关人员。一般框架会自带一些模块功能来支持发送邮件,我们需要进行一些组装。以Python为例:Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件以及带附件的邮件。 Python对SMTP支持有 smtplib 和 email 两个模块,email 负责构造邮件,smtplib 负责发送邮件。具体实现细节可以参看:Python实现将测试结果报告打包发送邮件

最后我们可以将测试执行、生成报告、发送邮件的调用方法进行封装并统一加入到run.py中,这样只需要执行 python run.py即可。


6 实践中遇到的问题及解决办法

以上已经了解了完整的测试框架及运行流程。只是一些基本的实现。真正运用到项目中,我们经常会遇到一些问题,在这里我也想跟大家分享一下我们经历的部分问题。

问题1:页面元素需要一定的操作后才出现,无法获取元素

例子:比如一些非select类型的下拉框,默认只显示一页数据(20条),如果超出一页数据且想要选中最后一个选项,需要滚动到最后才能在elements中看到该元素。

解决办法:那么就先滚动到最后,再获取元素。思路就是滚动前先获取下拉框的列表长度,如果滚动后的列表长度不等于滚动前的,就继续滚动到最后,直至相等。这里不得不提xpath定位中很好用的一个方法:last(),比如获取最后一个a元素,a[last()]。

问题2:同一个模块中用例执行时有些前置步骤重复操作且耗时

例子:为了保证用例之间隔离独立性,在实现上如果运行某个模块的测试用例之前,都需要做一些前置操作,比如打开tab页面,再打开navigator等。但这样做很浪费时间,因为在该模块运行前一个用例时这些操作已经做过了,如果再来一遍可能又需要耗费十几秒。

解决办法:运用pytest框架的fixture特性,将某些前置操作封装成fixture方法写入conftest.py中,并设置scope作用域为module,这样一旦模块中有用例调用过,其他用例就不会重复去操作了。

问题3:为了保证测试之间的独立性,一些编辑、删除操作前的数据构造是通过UI新建来进行的,很冗余

例子:比如UI上的业务数据表单需要覆盖新建、编辑、删除等操作,为了保证测试之间的独立性,编辑和删除之前都会先重复调用在UI上新建的流程,再进行编辑、删除,很冗余且耗时。

解决办法:封装一个post接口请求方法,通过接口快速的创建前置数据,把UI测试重点放在编辑和删除操作上。


参考资料

基于一些项目安全因素,我不能将代码框架分享给大家,但是强推以下链接,也是我学习参考的部分资料,思路跟我们项目的实现大体差不多,本文也运用了其中的一些知识点,希望能帮助到大家~

基于Selenium与Pytest框架的Web UI自动化测试系统的设计与实现

Selenium+Pytest自动化测试框架实战

BDD行为驱动简介及Pytest-bdd基础使用

pytest教程

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值