不会编程,如何做自动化测试
又会测试又懂编程的测试人员还是稀缺的,多数组织可能也没有在这方面投入,所以,让普通测试工程师简单学习一下,就可以进行自动化测试,还是有市场需求的。之前困惑于普通测试工程师无法深度参与到自动化测试的案例编写工作中,因而搜索了一下,还是发现了一个叫pytest-play的pytest插件,使用该插件,只要按照规则编写YAML文件,就可以进行自动化的工作,而该插件的中文介绍几乎没有,因此,特意写了这篇文
前言
又会测试又懂编程的测试人员还是稀缺的,多数组织可能也没有在这方面投入,所以,让普通测试工程师简单学习一下,就可以进行自动化测试,还是有市场需求的。
之前困惑于普通测试工程师无法深度参与到自动化测试的案例编写工作中,因而搜索了一下,还是发现了一个叫pytest-play的pytest插件,使用该插件,只要按照规则编写YAML文件,就可以进行自动化的工作,而该插件的中文介绍几乎没有,因此,特意写了这篇文章,向大家深度介绍一下。
能做什么
今天的主角叫pytest-play,从名称就可以看出,这是一个知名自动化测试框架pytest的插件。那么,这个插件能为我们做什么呢?
根据官方文档介绍,pytest-play可以用来做一些自动化的工作,其中当然包括自动化测试。
有什么特点
能做自动化测试的工具很多,pytest-play有什么特点呢?
首先,是非技术用户可用。这应该是pytest-play最大的特点和优势了,对于广大对编程不太精通的测试人员来说,只要通过学习一些相对简单的语法规则,就可以以编写清晰格式的文本文件的方式完成自动化测试任务,还是很香的。
其次,是测试配置化。我们知道,YAML是一个很好的文件格式,肯定用过它来编写测试案例的数据和参数等,而使用pytest-play,只要把测试动作、数据、断言也都写进去就可以了。
再次,是可扩展。pytest-play本身作为pytest的一个扩展插件,又被设计为可扩展的。目前,已知有play_selenium插件,可驱动浏览器进行Web页面测试,有play_requests驱动发起HTTP请求进行HTTP接口测试,play_sql可以执行SQL并对结果断言。当然,也可以自己开发需要的插件。
以上是pytest-play的主要特点,其它还有易于安装、易于使用、参数化、集成报告、与测试管理工具集成、学习曲线平滑等特点,不再详细介绍。
如何使用
pytest-play有两种使用方式,一种不用Python,另一种可以用。这样可以满足不同层级需求。
以test_开头的扩展名为yml的文件将被自动识别和运行,也可以使用名称或关键字运行整个测试集。
无Python方式
以下是一个无Python测试项目的示例,只包含一个可选的环境变量文件env-ALPHA.yml,和一个测试脚本文件test_login.yml。
$ tree . ├── env-ALPHA.yml (OPTIONAL) └── test_login.yml
以下是环境变量配置文件的内容,作为外置参数,可以用于在不同环境运行测试。
$ cat env-ALPHA.yml pytest-play: base_url: https://www.yoursite.com
以下是包含动作、断言和元数据(可选)的测试方案文件。
$ cat test_login.yml --- markers: - login test_data: - username: siteadmin password: siteadmin - username: editor password: editor - username: reader password: reader --- - comment: 访问初始页面 type: get provider: selenium url: "$base_url" - comment: 点击登录链接 locator: type: id value: personaltools-login type: clickElement provider: selenium - comment: 输入用户名 locator: type: id value: __ac_name text: "$username" type: setElementText provider: selenium - comment: 输入密码 locator: type: id value: __ac_password text: "$password" type: setElementText provider: selenium - comment: 点击登录按钮 locator: type: css value: ".pattern-modal-buttons > input[name=submit]" type: clickElement provider: selenium - comment: 等待页面加载完成 locator: type: css value: ".icon-user" type: waitForElementVisible provider: selenium
该示例文件用---分出了两段。
第一段为元数据段,非必须,可以用markers为当前测试设置若干标签,方便根据标签筛选运行测试,还可以使用test_data配置案例运行数据,本例中配置了3组测试数据,整个测试将运行3次。
第二段为测试脚本,用-配置了5个行为。 comment:是一条注释,简要说明本条,可输出到报告中,方便查看和编辑,非必须。 provider:用于指定要执行的命令,本例中为selenium,将调用selenium插件。 type:可作为provider的子命令,如waitForElementVisible为等待HTML元素可见。 locator:用于定位HTML元素,type为定位方式,value为定位参数。
有Python方式
此种方式可用于在使用pytest-bdd进行行为驱动测试。
以下示例为test_login.py的内容,定义了名为test_login函数,名为data的测试方案,最后调用play的execute_raw方法运行,extra_variables参数为环境变量类参数。
def test_login(play): data = play.get_file_contents( 'my', 'path', 'etc', 'login.yml') play.execute_raw(data, extra_variables={})
运行结果报告
在命令行中使用--junit-xml参数可生成junit格式的结果报告。
--junit-xml results.xml
在报告中,可以看到每个测试案例的错误、每次执行的命令、用时情况等。默认会看到每个命令行输出,除非运行时指定了-s或--capture=no参数。
以下为执行报告示例。
<?xml version="1.0" encoding="utf-8"?> <testsuite errors="0" failures="0" name="pytest" skipped="0" tests="1" time="0.360"> <testcase classname="test_assertion.yml" file="test_assertion.yml" name="test_assertion.yml" time="0.326"> <system-out>{'expression': '1 == 1', 'provider': 'python', 'type': 'assert', '_elapsed': 0.0003077983856201172} {'expression': '0 == 0', 'provider': 'python', 'type': 'assert', '_elapsed': 0.0002529621124267578} </system-out> </testcase> </testsuite>
结果报告是可以自定义的,可以在报告中输出自定义属性和执行用时。
以下使用play_requests插件测试HTTP接口的示例中,在断言之前插入了一段收集指标的自定义属性配置,provider为metrics,自定义属性名为categories_time,对应指标类型为record_elapsed(类似用时)。另外,示例针对自定义属性进行了断言。
test_data: - category: dev - category: movie - category: food --- - type: GET provider: play_requests url: https://api.chucknorris.io/jokes/categories expression: "'$category' in response.json()" - provider: metrics type: record_elapsed name: categories_time - type: assert provider: python expression: "variables['categories_time'] < 2.5" comment: 你可以在断言中使用categories_time
输出的报告如下。可以看到,输出了categories_time自定义属性及其值。
<?xml version="1.0" encoding="utf-8"?> <testsuite errors="0" failures="0" name="pytest" skipped="0" tests="3" time="2.031"> <testcase classname="test_categories.yml" file="test_categories.yml" name="test_categories.yml0" time="0.968"> <properties> <property name="categories_time" value="0.5829994678497314"/> </properties> <system-out>{'expression': "'dev' in response.json()", 'provider': 'play_requests', 'type': 'GET', 'url': 'https://api.chucknorris.io/jokes/categories', '_elapsed': 0.5829994678497314} {'name': 'categories_time', 'provider': 'metrics', 'type': 'record_elapsed', '_elapsed': 3.3855438232421875e-05} {'comment': '你可以在断言中使用categories_time', 'expression': "variables['categories_time'] < 2.5", 'provider': 'python', 'type': 'assert', '_elapsed': 0.0006382465362548828} </system-out> </testcase> <testcase classname="test_categories.yml" file="test_categories.yml" name="test_categories.yml1" time="0.481"> <properties> <property name="categories_time" value="0.4184422492980957"/> </properties> <system-out>{'expression': "'movie' in response.json()", 'provider': 'play_requests', 'type': 'GET', 'url': 'https://api.chucknorris.io/jokes/categories', '_elapsed': 0.4184422492980957} {'name': 'categories_time', 'provider': 'metrics', 'type': 'record_elapsed', '_elapsed': 2.09808349609375e-05} {'comment': '你可以在断言中使用categories_time', 'expression': "variables['categories_time'] < 2.5", 'provider': 'python', 'type': 'assert', '_elapsed': 0.000553131103515625} </system-out> </testcase> <testcase classname="test_categories.yml" file="test_categories.yml" name="test_categories.yml2" time="0.534"> <properties> <property name="categories_time" value="0.463592529296875"/> </properties> <system-out>{'expression': "'food' in response.json()", 'provider': 'play_requests', 'type': 'GET', 'url': 'https://api.chucknorris.io/jokes/categories', '_elapsed': 0.463592529296875} {'name': 'categories_time', 'provider': 'metrics', 'type': 'record_elapsed', '_elapsed': 2.09808349609375e-05} {'comment': '你可以在断言中使用categories_time', 'expression': "variables['categories_time'] < 2.5", 'provider': 'python', 'type': 'assert', '_elapsed': 0.00054931640625} </system-out> </testcase> </testsuite>
如何复用测试步骤
不同的测试方案中,应该存在可复用的测试步骤,这样,修改的时候,改一个地方就可以了。pytest-play考虑到了这一点,利用include这个provider实现了测试步骤的复用。
以下示例中,provider、type均为include关键字,测试方案复用了path中指定的共用测试步骤。
- provider: include type: include path: "/some-path/included-scenario.yml"
当然,如果测试步骤中有变量,可以在这些测试脚本的共同根目录下配置。
默认命令(命令的复用)
类似于代码中的类型继承,pytest-play也提供了测试命令复用的机制。
如使用play_requests测试同一服务的多个接口时,每次调用都需要传入相同的header,这种情况就可以使用默认命令来实现。
以下示例中,第1节通过python命令的store_variable子命令,定义了名为bearer,值为BEARER的变量。第2节中使用python命令的store_variable子命令,定义了名为play_requests的默认命令,在expression中设置了公用的HTTP头。第3节中直接复用了play_requests命令,就不需要再定义HTTP头了。
- provider: python type: store_variable name: bearer expression: "'BEARER'" - provider: python type: store_variable name: play_requests expression: "{'parameters': {'headers': {'Authorization': '$bearer'}}}" - provider: play_requests type: GET comment: this is an authenticated request! url: "$base_url"
声明变量、断言
在上面的示例中也看到了,我们可以在脚本中使用python命令的store_variable子命令声明变量,供后续步骤使用。
如下示例中,声明了名为foo的变量,其值通过expression表达式设置。该表达式是可计算的,最终值为2。后续测试步骤中,通过python命令的assert子命令进行了断言。
- provider: python type: store_variable expression: "1+1" name: foo - provider: python type: assert expression: variables['foo'] == 2
睡眠
在测试过程中,有时需要暂停运行一段时间,pytest-play也是支持的。下面的示例将暂停运行2秒钟。
- provider: python type: sleep seconds: 2
执行表达式
pytest-play支持通过python命令的exec子命令单纯地执行一个表达式。
- provider: python type: exec expression: "1+1"
循环
未支持复杂测试步骤,pytest-play支持循环运行一段测试步骤。
在如下示例中,子命令为while,表示这是一个循环。相邻的expression为进入循环的条件,要求变量countdown大于等于0,timeout表示循环的超时时间为2.3秒,poll表示每隔0.1秒判断一次,sub_commands表示循环体,里面为要循环执行的测试步骤。
- provider: python type: while expression: variables['countdown'] >= 0 timeout: 2.3 poll: 0.1 sub_commands: - provider: python type: store_variable name: countdown expression: variables['countdown'] - 1
条件步骤
pytest-play也支持根据条件判断是否跳过当前测试步骤。在以下示例中,skip_condition中的表达式为跳过当前命令的条件。
- provider: include type: include path: "/some-path/assertions.yml" skip_condition: variables['cassandra_assertions'] is True
对累计时间进行断言
pytest-play维护了一个名为_elapsed的变量,表示测试开始后经过的时间,可以在断言中使用。
- type: GET provider: play_requests url: https://api.chucknorris.io/jokes/categories expression: "'dev' in response.json()" - type: assert provider: python expression: "variables['_elapsed'] > 0"
时间跟踪
有时,我们需要知道每个测试命令执行耗时,pytest-play也是支持的。
在如下示例中,通过metrics命令的子命令record_elapsed_start、record_elapsed_stop分别记录步骤的起止时间,分别赋予load_time和live_search_time两个指标。
- provider: selenium type: get url: https://www.plone-demo.info/ - provider: metrics type: record_elapsed_start name: load_time - provider: selenium type: setElementText text: plone 5 locator: type: id value: searchGadget - provider: metrics type: record_elapsed_stop name: load_time - provider: metrics type: record_elapsed_start name: live_search_time - provider: selenium type: waitForElementVisible locator: type: css value: li[data-url$="https://www.plone-demo.info/front-page"] - provider: metrics type: record_elapsed_stop name: live_search_time
以下为运行后的结果报告,可以看到两个指标的值,分别约为1.1秒和1.09秒。
<?xml version="1.0" encoding="utf-8"?> <testsuite errors="0" failures="0" name="pytest" skipped="0" tests="1" time="13.650"> <testcase classname="test_search.yml" file="test_search.yml" name="test_search.yml" time="13.580"> <properties> <property name="load_time" value="1.1175920963287354"/> <property name="live_search_time" value="1.0871295928955078"/> </properties> <system-out>{'provider': 'selenium', 'type': 'get', 'url': 'https://www.plone-demo.info/', '_elapsed': 9.593282461166382} {'name': 'load_time', 'provider': 'metrics', 'type': 'record_elapsed_start', '_elapsed': 1.1682510375976562e-05} {'locator': {'type': 'id', 'value': 'searchGadget'}, 'provider': 'selenium', 'text': 'plone 5', 'type': 'setElementText', '_elapsed': 1.1019845008850098} {'name': 'load_time', 'provider': 'metrics', 'type': 'record_elapsed_stop', '_elapsed': 1.9788742065429688e-05} {'name': 'live_search_time', 'provider': 'metrics', 'type': 'record_elapsed_start', '_elapsed': 1.0013580322265625e-05} {'locator': {'type': 'css', 'value': 'li[data-url$="https://www.plone-demo.info/front-page"]'}, 'provider': 'selenium', 'type': 'waitForElementVisible', '_elapsed': 1.060795545578003} {'name': 'live_search_time', 'provider': 'metrics', 'type': 'record_elapsed_stop', '_elapsed': 2.3603439331054688e-05} </system-out> </testcase> </testsuite>
测量时间可以用于预测系统行为,比较不同分支或场景下的性能。除了使用pytest-play自带的上述方式外,也可以安装pytest-statsd插件和StatsD、Graphite-Web集成达到该目的。
StatsD、Graphite-Web可以使用Docker部署。参见: https://graphite.readthedocs.io/en/latest/install.html
pytest-statsd插件安装命令如下:
pip install pytest-play[statsd]
用法如下:
--stats-d [--stats-prefix play --stats-host http://myserver.com --stats-port 3000]
--stats-d用于启用统计数据,--stats-host指定接收指标数据的目标服务器HTTP地址,--stats-port指定目标服务器的端口,--stats-prefix用于目标服务器是公用的情况,用来区分源。
为了收集和发送指标数据,可以使用record_elapsed、record_elapsed_start、record_elapsed_stop等命令,也可以使用record_property命令,但是必须同时提供metric_type附加参数,具体用法如下:- provider: metrics type: record_property name: categories_time expression: "variables['_elapsed']*1000" metric_type: timing - provider: metrics type: record_property name: fridge_temperature expression: "4" metric_type: gauge
如果未提供metric_type,则指标数据不会发送到StatsD。
以下为最终生成的统计图形示例。

HTTP接口响应时间示例

性能测试
可以利用bzt/Taurus将pytest-play测试方案用作性能测试。
具体方法是,添加一个bzt/Taurus的YAML文件,需要注意文件名称不能以test_开头。这里有一个完整的示例: https://github.com/pytest-dev/pytest-play/tree/features/examples/bzt_performance
settings: artifacts-dir: /tmp/%Y-%m-%d_%H-%M-%S.%f execution: - executor: pytest scenario: pytest-run iterations: 1 scenarios: pytest-run: # additional-args: --stats-d --stats-prefix play script: scripts/ services: - module: shellexec prepare: - pip3 install -r https://raw.githubusercontent.com/davidemoro/pytest-play-docker/master/requirements.txt
运行方法如下:
docker run --rm -it -v $(pwd):/src --user root --entrypoint "bzt" davidemoro/pytest-play bzt.yml
命令执行后,可以看到bzt启动,并运行各测试场景:

在消息体中直接使用动态数据
通常使用store_variable子命令来提供变量支持,这种方式的变量可以在多处使用。有时,需要在发送消息时(REST或MQTT)临时生成一个数值,就要使用 { ! 表达式 ! } 这种声明一个表达式,示例如下。
- comment: python expressions in mqtt payload (without declaring variables) provider: mqtt type: publish host: "$mqtt_host" port: "$mqtt_port" endpoint: "$mqtt_endpoint/$device_serial_number" payload: '{ "measure_id": [124], "obj_id_L": [0], "measureType": ["float"], "start_time": {! int(datetime.datetime.utcnow().timestamp()*1000) !}, "bin_value": [1] }'
扩展
pytest-play提供了一套扩展机制,支持自定义命令。
扩展命令的方式如下:command = {'type': 'print', 'provider': 'newprovider', 'message': 'Hello, World!'}
同时还需要实现该命令提供程序:
from pytest_play.providers import BaseProvider class NewProvider(BaseProvider): def this_is_not_a_command(self): """ Commands should be command_ prefixed """ def command_print(self, command): print(command['message']) def command_yetAnotherCommand(self, command): print(command)
并在setup.py中注册:
entry_points={ 'playcommands': [ 'print = your_package.providers:NewProvider', ], },
示例
以下是一些示例的地址:
-
https://github.com/pytest-dev/pytest-play/tree/master/examples
-
https://github.com/davidemoro/pytest-play-docker/tree/master/tests
-
https://github.com/davidemoro/pytest-play-plone-example
插件
目前已有的第三方插件如下:
-
play_selenium:通过Selenium/Splinter驱动浏览器测试Web页面
-
play_requests:驱动Python的requests库发送请求测试HTTP接口
-
play_sql:支持访问SQL数据库并断言
-
play_cassandra:支持Cassandra分布式NoSQL数据库
-
play_dynamodb:对AWS DynamoDB查询和断言
-
play_websocket:对websockets进行支持
-
play_mqtt:提供对MQTT的支持
更多推荐




所有评论(0)