演练:创建web自动化测试项目
这一演练中将会介绍如何使用CukeTest自动化工具实现一个小型ERP系统的自动化测试,实现录制=>场景=>参数化=>数据驱动的整个流程 。
通过这个介绍,可以让大家初步了解:
- Web自动化测试录制
- 行为驱动框架BDD的开发
- 测试场景的编写和实现
- 参数化和数据驱动
准备工作
环境准备
启动ERP演示系统(CukeTest自带):在CukeTest主窗口的工具
菜单下,点击启动Web样例
- DemoErp
,通过本机浏览器打开ERP应用。默认的用户名admin
、密码admin
。将网址复制下来。
创建项目
打开CukeTest,使用"Basic"模板创建一个项目,项目名称为"orders"。 "Basic" 模板提供了Web界面测试相关的Feature文件、pytest-bdd格式代码。
快速开发
设计操作流程
登录后ERP页面中的全部订单就是本次要测试的功能模块。观察这个简单的页面,可以很快的设计出来一个基本的测试流程,共四步:
- 进入订单管理平台:最基本的登录操作。允许登录不同的账号。
- 订单录入:自动填写表单并提交。
- 验证订单录入结果:查验订单检查是否有被正确录入。
- 删除订单:删除指定的订单。
录制Web操作
测试用例设计好后,可以通过CukeTest的录制功能,将用例中的步骤操作录制为自动化脚本。可以尝试一次性录制完一个完整的操作流程,即:
- 登录
- 新建订单
- 填写订单
- 删除订单
录制一个闭环的自动化流程好处非常多,最直观的好处就是可以重复运行,每次运行都会把新建的测试数据(第2步)删除(第4步)。
那么现在只需要启动录制,然后手动执行一遍上面的操作流程即可。
首先,在主界面点击录制设置,将被测网页的URL填进去(也可以先不填写,开始录制后在浏览器地址栏手动输入即可),就可以开始录制。
进入录制后,鼠标悬停在页面中可以看到该网页元素的选择器Selector
,并且用红色背景标记该元素的区域,接着对该元素的操作都会被录制下来并生成相应的操作代码,无论是鼠标点击还是键盘输入。
录制时的界面如下:
完成流程操作后,我们会得到一段这样的代码(注释有删改):
from leanproWeb import WebAuto
def run(webauto: WebAuto) -> None:
browser = webauto.chromium.launch(headless=False)
context = browser.new_context()
# 访问主页
page = context.new_page()
page.goto("http://localhost:20345/")
# 执行登录
page.goto("http://localhost:20345/user/login")
page.click("[placeholder=\"用户名: admin or user\"]")
page.fill("[placeholder=\"用户名: admin or user\"]", "admin")
page.press("[placeholder=\"用户名: admin or user\"]", "Tab")
page.fill("[placeholder=\"密码: admin\"]", "admin")
with page.expect_navigation():
page.click("button:has-text(\"登 录\")")
# 新建订单
page.click("button:has-text(\"新建\")")
page.click("[placeholder=\"请输入订单编号\"]")
page.fill("[placeholder=\"请输入订单编号\"]", "13920230210")
page.click(".ant-picker")
page.click("text=22")
page.click("input[role=\"combobox\"]")
page.click(":nth-match(:text(\"Harber Group and Sons.\"), 2)")
page.click("text=客户Harber Group and Sons.Harber Group and Sons.交货日期 >> [placeholder=\"请选择\"]")
page.click("tbody div:has-text(\"23\")")
page.click("[placeholder=\"请填写\"]")
page.fill("[placeholder=\"请填写\"]", "shanghai")
page.click("[placeholder=\"请输入\"]")
page.fill("[placeholder=\"请输入\"]", "wang")
page.click("#phone")
page.fill("#phone", "11234553334")
page.click("#total")
page.fill("#total", "100")
page.click("button:has-text(\"提 交\")")
# 删除订单
page.click("text=123456")
page.click("button:has-text(\"删除\")")
# 关闭页面
page.close()
context.close()
browser.close()
with WebAuto() as webauto:
run(webauto)
录制生成的脚本可以直接点击【运行脚本】来回放,如果需要减缓回放的速度来观察操作过程,可以在chromium.lauch()
方法中指定一个slowMo
属性,单位为毫秒(ms):
browser = webauto.chromium.launch(headless=False,slow_mo=2000)
改进代码
上面一个阶段完成后,我们有了一份可执行的python代码,已经可以完成进行特定场景的测试了。但是录制的代码只能完成既定的操作序列,不方便对运行过程做灵活的调整;使用起来也不够高效,无法在测试过程中使用不同的参数。
怎么做才能避免以上问题,从而更好地完成自动化测试任务呢?
对于这个问题,我们建议使用场景来管理脚本,而这需要以一定的格式来组织脚本,来将场景与脚本匹配起来。 通过这种方式组织录好的脚本有以下好处:
- 使用场景来管理可以保证当测试项目变大也不会变得混乱,每个场景、每个步骤间的作用域隔离,内部不会产生数据错乱。
- 进一步拆分为步骤后还可以使用参数化、数据驱动等测试功能。
- 可以使用各种Hook来在各个运行的特定时间点插入操作脚本,如截图、清理、校验等常用的测试操作。
- 运行结束后可以看到运行报告,运行过程中的所有结果都会在报告中用图表信息一并呈现,方便工程师查看测试情况。
下面,我们分3个步骤来实现:
- 格式改造
- 参数化处理
- 实现数据驱动
格式改造
对测试方法比较熟悉的读者应该已经发现,这个格式其实就是行为驱动开发(Behavior Drive Development)测试的格式。因此我们首先需要将一开始定义的测试流程,变为一个剧本文件:
打开项目中的剧本文件feature1.feature
编写这几个场景的测试用例:
- 登录
- 新建订单
- 填写订单
- 删除订单
编辑测试用例时可以点击剧本区域右上角将 CukeTest 切换到文本界面,直接将如下代码复制进去。
# language: zh-CN
功能: ERP订单自动录入
从excel表中读取订单数据并自动录入,将执行结果导出到excel
场景: 进入订单管理平台
假如打开网址"DemoErp"样例
那么输入用户名"admin",密码"admin",登录账号导航到指定页面
场景: 订单录入
那么点击“新建”按钮,读取excel文件"./support/order.xlsx",根据"SAL20210315026"将订单数据录入到系统
场景: 删除订单
假如删除订单"SAL20210315026"
在“订单录入”这个场景中,由于需要表单中填写的数据较多,因此将数据维护在Excel文件中,在填写时读取出来写进表单里。
创建conftest.py
文件。在pytest中,这个文件是一个全局配置文件,它可以用来定义全局的fixture,以及一些自定义的设置和插件。在做界面自动化测试时,我们通常将被测应用的启动和关闭,以及测试环境的初始化放在这里。内容如下:
from leanproWeb import WebAuto
from auto.sync_api import sync_auto
import base64
browser = WebAuto().__enter__().chromium.launch(headless=False)
context = browser.new_context()
page = browser.new_page()
def pytest_sessionfinish(session):
browser.close()
context.close()
page.close()
def pytest_bdd_after_scenario(request, feature, scenario):
...
test_feature1.py
文件根据每个场景手动编写对应的测试用例函数并确保遵守pytest-bdd结构规范:
from pytest_bdd import scenarios, given, when, then, parsers
import pytest
from conftest import page
scenarios("../features")
@given('打开网址DemoErp样例')
def goto_home():
page.goto("http://localhost:20345/")
@then(parsers.parse('输入用户名"{username}",密码"{password}",登录账号导航到指定页面'))
def login(username, password):
page.goto("http://localhost:20345/user/login")
page.click("[placeholder=\"用户名: admin or user\"]")
page.fill("[placeholder=\"用户名: admin or user\"]", "admin")
page.press("[placeholder=\"用户名: admin or user\"]", "Tab")
page.fill("[placeholder=\"密码: admin\"]", "admin")
with page.expect_navigation():
page.click("button:has-text(\"登 录\")")
@then(parsers.parse('点击“新建”按钮,读取excel文件"{xlsx_file}",根据"{order_no}"将订单数据录入到系统'))
def new_order(xlsx_file, order_no):
page.click("button:has-text(\"新建\")")
page.click("[placeholder=\"请输入订单编号\"]")
page.fill("[placeholder=\"请输入订单编号\"]", "123456")
page.click(".ant-picker")
page.click("text=22")
page.click("input[role=\"combobox\"]")
page.click(":nth-match(:text(\"Harber Group and Sons.\"), 2)")
page.click("text=客户Harber Group and Sons.Harber Group and Sons.交货日期 >> [placeholder=\"请选择\"]")
page.click("tbody div:has-text(\"23\")")
page.click("[placeholder=\"请填写\"]")
page.fill("[placeholder=\"请填写\"]", "上海市")
page.click("[placeholder=\"请输入\"]")
page.fill("[placeholder=\"请输入\"]", "张三")
page.click("#phone")
page.fill("#phone", "11234553334")
page.click("#total")
page.fill("#total", "100")
page.click("button:has-text(\"提 交\")")
@given(parsers.parse('删除订单"{order_no}"'))
def delete_order(result, order_no):
page.click("text=123456")
page.click("button:has-text(\"删除\")")
运行项目&获得报告
将录制的脚本放入新的步骤定义脚本中后,就可以作为项目运行了。点击界面上的“运行项目”按钮,可以看到项目顺利启动,执行的操作并没有发生变化,但是运行结束后生成了一分运行报告,如下:
这个报告中详细的记录了每个步骤的结果和状态,并且可以点开查看更加具体的信息。
参数化处理
尽管项目目前已经可以运行,并生成了相应的运行报告,但这并不代表任务已经完成。实际上,我们离达到最终目标已经非常接近。
在仔细观察上述步骤中定义的脚本时,我们注意到了{username}这样的占位符的使用。这些占位符允许我们以动态的方式传递具体数值,而不是在测试中预先硬编码。parsers.parse则是一个强大的工具,能够灵活解析和获取这些数值,并在函数中填充相应的参数。解析出的参数将被动态传递到相应的函数中,为测试用例提供了高度的灵活性和可维护性。
@then(parsers.parse('输入用户名"{username}",密码"{password}",登录账号导航到指定页面'))
def login(username, password):
page.goto("http://localhost:20345/user/login")
page.click("[placeholder=\"用户名: admin or user\"]")
page.fill("[placeholder=\"用户名: admin or user\"]", username)
page.press("[placeholder=\"用户名: admin or user\"]", "Tab")
page.fill("[placeholder=\"密码: admin\"]", password)
with page.expect_navigation():
page.click("button:has-text(\"登 录\")")
[placeholder="用户名: admin or user"]
和[placeholder="密码: admin"]
,替换成各自输入框的id
选择器,则可以写作:
@then(parsers.parse('输入用户名"{username}",密码"{password}",登录账号导航到指定页面'))
def login(username, password):
page.goto('http://localhost:21216/user/login')
page.click("#username")
page.fill("#username", user)
page.click('#password')
page.fill('#password', pwd)
with page.expect_navigation():
page.click("button:has-text(\"登 录\")")
其它来源的参数
了解了步骤描述中的参数含义,那我们回到“新建订单”这个步骤,由于这一步需要将大量数据填入到表单中,如果将这些数据全部写在步骤中,那么步骤会变得非常的长而且难以复用。
因此这里选择将完整数据放在一个Excel文件中(当然也可以选择txt文件或csv文件);这样在步骤描述里只需要再引入一个索引值就行,这里选择的是“订单编号”。为了实现这个目的,我们需要编写读取Excel文件和根据订单号索引数据的脚本:
data = read_xlsx(xlsx_file)
target_order = find_order(order_no, data)
其中
xlsx_file
是Excel文件的路径,order_no
是订单号。而得到的data
是Excel中的所有数据,target_order
是目标订单的数据。
结合到已有的步骤定义中,就可以写作:
@then(parsers.parse('点击“新建”按钮,读取excel文件"{xlsx_file}",根据"{order_no}"将订单数据录入到系统'))
def new_order(xlsx_file, order_no):
data = read_xlsx(xlsx_file)
target_order = find_order(order_no, data)
page.click("button:has-text(\"新建\")")
page.click("[placeholder=\"请输入订单编号\"]")
page.fill("[placeholder=\"请输入订单编号\"]", target_order["订单编号"]) # 填写订单编号
page.click('input#orderDate') # 填写订单日期
page.fill('input#orderDate', format_excel_time(target_order["订单日期"])) # 直接填写日期,formatExcelTime()用于处理Excel日期格式与表单格式的差异。
page.press('[placeholder="请选择"]', 'Enter')
page.click("input[role=\"combobox\"]") # 选择客户
page.click(":nth-match(:text(\"{0}\"), 2)".format(target_order["客户"]))
page.click('#deliveryDate') # 填写交货日期
page.fill('#deliveryDate', format_excel_time(target_order["交货日期"])) # // 直接填写日期,formatExcelTime()用于处理Excel日期格式与表单格式的差异。
page.press('[placeholder="请选择"]', 'Enter')
page.fill("[placeholder=\"请填写\"]", target_order["收货地址"]) # 填写收货地址
page.fill("[placeholder=\"请输入\"]", target_order["联系人"]) # 填写联系人
page.fill("#phone", target_order["电话"]) # 填写电话
page.fill("#total", str(target_order["金额总计(元)"])) # 填写金额总计
page.click("button:has-text(\"提 交\")")
read_xlsx()
方法、format_excel_time()
是样例中的自定义函数,具体函数实现可以查看学习样例orders
中的utils.js
文件。
到这一步,最困难的一步已经迈过,目前的项目已经非常完备。但如果需要让这个项目真正的代替测试工作、提高测试效率,还需要最后一步改进——数据驱动。
实现数据驱动
无论是测试需求还是日常工作需求,都要求能够录入多条订单,我们引入场景大纲这个功能。
所谓场景大纲是一种特殊的场景类型,通过定义一张示例表,场景大纲会自动使用表中的参数来运行,详见场景大纲(Scenario Outline)。以下是步骤:
- 右键单击场景“订单录入”的标题,选择“更改场景类型”=>“场景大纲”:这时步骤中的参数都会被汇总到一张表中,我们称作示例表,是场景大纲运行时的依据。
(可选)更新步骤和示例表中的参数名称:为了让步骤和数据表更易读,可以将默认的
param*
参数名改成合适的名称:- 在步骤文本和表格标题中将“param2”更改为“orderNo”。
- 删除
param1
列。由于Excel文件路径是固定的,因此可以删除掉
param1
列来,减少示例表的维护数量。
填充更多数据:将更多数据行添加到示例表中。双击表格主体,按Tab键直到导航到新行,然后填充一些新数据。编辑完成后,整个场景如下:
你也可点击剧本区域右上角将 CukeTest 切换到文本界面,直接将如下代码复制进去:
场景大纲: 订单录入
那么点击“新建”按钮,读取excel文件"./support/order.xlsx",根据"<orderNo>"将订单数据录入到系统
例子:
| orderNo |
| SAL20210315023 |
| SAL20210315026 |
| SAL20210315027 |
| SAL20210315028 |
这时,如果再次运行项目,就可以看到批量生成的场景运行结果:
至此,你就可以通过增加示例表的内容来完成任意多的订单录入工作,真正的解放双手。
正确性保障
当然啦,无论是用自动化代替重复劳动,还是用自动化测试保证软件质量,都需要保证操作的正确性,这需要我们加入更多的代码,包括:
- 验证订单录入结果:应该录入的订单必须成功录入;不应该录入的订单必须录入失败。
- 验证订单删除结果:只删除目标订单,而不会删除掉不相关的订单。
- 截图或录屏留档:在录制整个运行过程,由于Web自动化操作速度较快,还可以在每个场景结束后截图显示在报告中。
对于前两个功能的实现可以查看学习样例orders
中的源码,第三个功能可以借助CukeTest提供的API快速完成。
截图和录屏
屏幕截图和录像对 UI 测试很有帮助,这使测试人员确信自动化确实在做期望的工作。
如果希望在场景结束时抓取浏览器截图,借助CukeTest左侧“工具箱”标签页提供的After Hook
可以非常轻松的写出来,将“PyTest” -> “pytest_bdd_after_scenario
”拖拽到步骤定义的空白处,在加上控件截图操作,用request.attach()
将截图作为附件添加到报告中代码如下:
# conftest.py
def pytest_bdd_after_scenario(request, feature, scenario):
# TODO: 附件到报告
screenshot = page.screenshot()
screen = base64.b64encode(screenshot).decode('utf-8')
request.attach(screen, "image/png")
录屏就更加简单了,在 CukeTest 中,你还可以通过启动运行配置中的“录制视频”选项,在测试运行期间录制视频。
总结
软件测试是软件质量保证的关键,直接影响软件的质量评价。而ERP系统伴随着业务规模扩大,会变成体量大、流程复杂的庞大软件,无论是其质量保障还是自动化都是一种挑战,而CukeTest正是对抗这种挑战的工具。