演练:创建web自动化测试项目

这一演练中将会介绍如何使用CukeTest自动化工具实现一个小型ERP系统的自动化测试,实现录制=>场景=>参数化=>数据驱动的整个流程 。

通过这个介绍,可以让大家初步了解:

  • Web自动化测试录制
  • 行为驱动框架BDD的开发
  • 测试场景的编写和实现
  • 参数化和数据驱动

准备工作

环境准备

启动ERP演示系统(CukeTest自带):在CukeTest主窗口的工具菜单下,点击启动Web样例 - DemoErp,通过本机浏览器打开ERP应用。默认的用户名admin、密码admin。将网址复制下来。

创建项目

打开CukeTest,使用"Basic"模板创建一个项目,项目名称为"orders"。 "Basic" 模板提供了Web界面测试相关的Feature文件、pytest-bdd格式代码。

创建Web模板项目

快速开发

设计操作流程

登录后ERP页面中的全部订单就是本次要测试的功能模块。观察这个简单的页面,可以很快的设计出来一个基本的测试流程,共四步:

  1. 进入订单管理平台:最基本的登录操作。允许登录不同的账号。
  2. 订单录入:自动填写表单并提交。
  3. 验证订单录入结果:查验订单检查是否有被正确录入。
  4. 删除订单:删除指定的订单。

录制Web操作

测试用例设计好后,可以通过CukeTest的录制功能,将用例中的步骤操作录制为自动化脚本。可以尝试一次性录制完一个完整的操作流程,即:

  1. 登录
  2. 新建订单
  3. 填写订单
  4. 删除订单

录制一个闭环的自动化流程好处非常多,最直观的好处就是可以重复运行,每次运行都会把新建的测试数据(第2步)删除(第4步)。

那么现在只需要启动录制,然后手动执行一遍上面的操作流程即可。

首先,在主界面点击录制设置,将被测网页的URL填进去(也可以先不填写,开始录制后在浏览器地址栏手动输入即可),就可以开始录制。

录制设置界面

进入录制后,鼠标悬停在页面中可以看到该网页元素的选择器Selector,并且用红色背景标记该元素的区域,接着对该元素的操作都会被录制下来并生成相应的操作代码,无论是鼠标点击还是键盘输入。

录制时的界面如下: 录制操作

完成流程操作后,我们会得到一段这样的代码(注释有删改):

Python
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):

Python
browser = webauto.chromium.launch(headless=False,slow_mo=2000)

改进代码

上面一个阶段完成后,我们有了一份可执行的python代码,已经可以完成进行特定场景的测试了。但是录制的代码只能完成既定的操作序列,不方便对运行过程做灵活的调整;使用起来也不够高效,无法在测试过程中使用不同的参数。

怎么做才能避免以上问题,从而更好地完成自动化测试任务呢?

对于这个问题,我们建议使用场景来管理脚本,而这需要以一定的格式来组织脚本,来将场景与脚本匹配起来。 通过这种方式组织录好的脚本有以下好处:

  1. 使用场景来管理可以保证当测试项目变大也不会变得混乱,每个场景、每个步骤间的作用域隔离,内部不会产生数据错乱。
  2. 进一步拆分为步骤后还可以使用参数化、数据驱动等测试功能。
  3. 可以使用各种Hook来在各个运行的特定时间点插入操作脚本,如截图、清理、校验等常用的测试操作。
  4. 运行结束后可以看到运行报告,运行过程中的所有结果都会在报告中用图表信息一并呈现,方便工程师查看测试情况。

下面,我们分3个步骤来实现:

  • 格式改造
  • 参数化处理
  • 实现数据驱动

格式改造

对测试方法比较熟悉的读者应该已经发现,这个格式其实就是行为驱动开发(Behavior Drive Development)测试的格式。因此我们首先需要将一开始定义的测试流程,变为一个剧本文件: 打开项目中的剧本文件feature1.feature编写这几个场景的测试用例:

  1. 登录
  2. 新建订单
  3. 填写订单
  4. 删除订单

编辑测试用例时可以点击剧本区域右上角将 CukeTest 切换到文本界面,直接将如下代码复制进去。

# language: zh-CN
功能: ERP订单自动录入
从excel表中读取订单数据并自动录入,将执行结果导出到excel

  场景: 进入订单管理平台
    假如打开网址"DemoErp"样例
    那么输入用户名"admin",密码"admin",登录账号导航到指定页面

  场景: 订单录入
    那么点击“新建”按钮,读取excel文件"./support/order.xlsx",根据"SAL20210315026"将订单数据录入到系统

  场景: 删除订单
    假如删除订单"SAL20210315026"

在“订单录入”这个场景中,由于需要表单中填写的数据较多,因此将数据维护在Excel文件中,在填写时读取出来写进表单里。

创建conftest.py文件。在pytest中,这个文件是一个全局配置文件,它可以用来定义全局的fixture,以及一些自定义的设置和插件。在做界面自动化测试时,我们通常将被测应用的启动和关闭,以及测试环境的初始化放在这里。内容如下:

Python
# conftest.py
from leanproWeb import WebAuto
import pytest
import base64

# 定义一个fixture,启动一个浏览器实例,作用域为整个会话
@pytest.fixture(scope="session")
def browser():
    # 使用WebAuto库启动一个Chromium浏览器实例,`headless=False`表示浏览器将以有头模式启动(即浏览器界面可见)
    browser = WebAuto().__enter__().chromium.launch(headless=False)
    yield browser
    # 关闭浏览器实例、浏览上下文和页面,释放资源。
    browser.close()


# 定义另一个fixture,用于在浏览器中创建一个新页面,作用域为整个会话
@pytest.fixture(scope="session")
def page(browser):
    # 在浏览器中创建一个新的浏览上下文
    context = browser.new_context()

    # 在当前浏览器实例中打开一个新的页面(或标签页)
    page = browser.new_page()
    yield page
    context.close()

# pytest钩子函数,会在测试会话结束时调用,用于执行清理工作,如关闭浏览器
def pytest_sessionfinish(session):
    print("Test session finished. Resources are released.")

# 每个场景测试结束后调用,用于执行特定操作,如捕获屏幕截图
def pytest_bdd_after_scenario(request, feature, scenario):
    # 通过request对象获取页面fixture,方便对页面进行操作
    page = request.getfixturevalue('page')
    # 截取当前页面的屏幕截图
    screenshot = page.screenshot()
    # 将截图转换为base64编码的字符串
    screen = base64.b64encode(screenshot).decode('utf-8')

    # 将截图附加到测试报告中,MIME类型为"image/png"
    request.attach(screen, "image/png")
关于fixture的详细用法可以参考文档如何使用 fixtures

打开test_feature1.py文件根据每个场景手动编写对应的测试用例函数并确保遵守pytest-bdd结构规范:

Python
from pytest_bdd import scenarios, given, when, then, parsers
import pytest

scenarios("../features")

@given('打开网址DemoErp样例')
def goto_home(page):
    page.goto("http://localhost:20345/")

@then(parsers.parse('输入用户名"{username}",密码"{password}",登录账号导航到指定页面'))
def login(page, 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(page, 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(page, result, order_no):
    page.click("text=123456")
    page.click("button:has-text(\"删除\")")

运行项目&获得报告

将录制的脚本放入新的步骤定义脚本中后,就可以作为项目运行了。点击界面上的“运行项目”按钮,可以看到项目顺利启动,执行的操作并没有发生变化,但是运行结束后生成了一分运行报告,如下:

初步的运行报告

这个报告中详细的记录了每个步骤的结果和状态,并且可以点开查看更加具体的信息。

参数化处理

尽管项目目前已经可以运行,并生成了相应的运行报告,但这并不代表任务已经完成。实际上,我们离达到最终目标已经非常接近。

在仔细观察上述步骤中定义的脚本时,我们注意到了{username}这样的占位符的使用。这些占位符允许我们以动态的方式传递具体数值,而不是在测试中预先硬编码。parsers.parse则是一个强大的工具,能够灵活解析和获取这些数值,并在函数中填充相应的参数。解析出的参数将被动态传递到相应的函数中,为测试用例提供了高度的灵活性和可维护性。

Python
@then(parsers.parse('输入用户名"{username}",密码"{password}",登录账号导航到指定页面'))
def login(page, 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选择器,则可以写作:

Python
@then(parsers.parse('输入用户名"{username}",密码"{password}",登录账号导航到指定页面'))
def login(page, 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文件根据订单号索引数据的脚本:

Python
data = read_xlsx(xlsx_file)
target_order = find_order(order_no, data)

其中xlsx_file是Excel文件的路径,order_no是订单号。而得到的data是Excel中的所有数据,target_order是目标订单的数据。

结合到已有的步骤定义中,就可以写作:

Python
@then(parsers.parse('点击“新建”按钮,读取excel文件"{xlsx_file}",根据"{order_no}"将订单数据录入到系统'))
def new_order(page, 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)。以下是步骤:

  1. 右键单击场景“订单录入”的标题,选择“更改场景类型”=>“场景大纲”:这时步骤中的参数都会被汇总到一张表中,我们称作示例表,是场景大纲运行时的依据。
  2. (可选)更新步骤和示例表中的参数名称:为了让步骤和数据表更易读,可以将默认的param*参数名改成合适的名称:

    • 在步骤文本和表格标题中将“param2”更改为“orderNo”。
    • 删除param1列。

      由于Excel文件路径是固定的,因此可以删除掉param1列来,减少示例表的维护数量。

  3. 填充更多数据:将更多数据行添加到示例表中。双击表格主体,按Tab键直到导航到新行,然后填充一些新数据。编辑完成后,整个场景如下:

转换为场景大纲

你也可点击剧本区域右上角将 CukeTest 切换到文本界面,直接将如下代码复制进去:

  场景大纲: 订单录入
    那么点击“新建”按钮,读取excel文件"./support/order.xlsx",根据"<orderNo>"将订单数据录入到系统
    例子: 
      | orderNo        |
      | SAL20210315023 |
      | SAL20210315026 |
      | SAL20210315027 |
      | SAL20210315028 |

这时,如果再次运行项目,就可以看到批量生成的场景运行结果: 场景大纲的运行结果

至此,你就可以通过增加示例表的内容来完成任意多的订单录入工作,真正的解放双手。

正确性保障

当然啦,无论是用自动化代替重复劳动,还是用自动化测试保证软件质量,都需要保证操作的正确性,这需要我们加入更多的代码,包括:

  1. 验证订单录入结果:应该录入的订单必须成功录入;不应该录入的订单必须录入失败。
  2. 验证订单删除结果:只删除目标订单,而不会删除掉不相关的订单。
  3. 截图或录屏留档:在录制整个运行过程,由于Web自动化操作速度较快,还可以在每个场景结束后截图显示在报告中。

对于前两个功能的实现可以查看学习样例orders中的源码,第三个功能可以借助CukeTest提供的API快速完成。

截图和录屏

屏幕截图和录像对 UI 测试很有帮助,这使测试人员确信自动化确实在做期望的工作。

如果希望在场景结束时抓取浏览器截图,借助CukeTest左侧“工具箱”标签页提供的After Hook可以非常轻松的写出来,将“PyTest” -> “pytest_bdd_after_scenario”拖拽到步骤定义的空白处,在加上控件截图操作,用request.attach()将截图作为附件添加到报告中代码如下:

Python
# 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正是对抗这种挑战的工具。

results matching ""

    No results matching ""