什么是 BDD 测试?行为驱动开发框架

⚡ 智能摘要

BDD 测试使用简单的 Given-When-Then 语言描述应用程序行为,以便分析师、开发人员和测试人员共享同一份规范。本演练将这种方法应用于使用 Behave 进行 REST API 测试。 Python 框架涵盖设置、功能文件、步骤实现、执行和报告。

  • 🗣️ 核心原则: 行为驱动开发扩展了测试驱动开发,它用非程序员也能阅读和认可的自然语言表达每一个场景。
  • 🧱 场景结构: Given 建立前提条件,When 执行操作,Then 断言结果,而 And 或 But 继承前一步的装饰器。
  • 🌐 REST 覆盖范围: 该示例通过 POST、GET、PUT 和 DELETE 对公共 jsonplaceholder posts 服务执行完整的 CRUD 行为。
  • 🐍 框架设置: Behave 通过 pip 安装。 Python 3 并期望包含功能文件的 features 目录以及包含实现的步骤包。
  • 🔗 步骤绑定: 诸如 @given、@when 和 @then 之类的装饰器与功能文本匹配,而带花括号的占位符则将场景中的值传递到功能文本中。 Python 功能。
  • ▶️ 执行机制: 美观的格式化程序会在控制台中打印出每个步骤的通过或失败结果,并使用颜色进行区分。
  • 📊 报告: allure-behave 格式化程序会生成机器可读的结果,Allure 命令行会将其渲染为可浏览的 HTML 报告。

什么是 BDD 测试

什么是 BDD(行为驱动开发)测试?

BDD(行为驱动开发)测试 BDD 是一种敏捷软件开发技术,是 TDD(测试驱动开发)的扩展。在 BDD 中,测试用例使用自然语言编写,即使非程序员也能阅读。

BDD 测试如何工作?

假设你被指派在网银应用程序中创建一个资金转账模块。

有多种方法可以测试它:

  1. 如果汇款账户余额充足,则应进行资金转账。
  2. 如果目的地账户详细信息正确,则应进行资金转账
  3. 如果用户输入的交易密码/RSA码/安全认证信息正确,则应进行资金转账。
  4. 即使是银行假日,资金转账也应该进行。
  5. 资金转账应在账户持有人设定的未来日期进行

测试场景 随着我们考虑诸如在Y天或Y个月的时间间隔内转账X金额、停止等附加功能,情况会变得更加复杂精细。ping 当总额达到 Z 时进行预定转账,依此类推。

开发人员的普遍倾向是先开发功能,然后再编写测试代码。正如上面的案例所示, 测试用例 这里的开发很复杂,所以开发商会推迟。 测试与验证 直到发布时,才会进行快速但无效的测试。

为了解决这个问题,行为驱动开发(BDD)应运而生。它简化了开发人员的整个测试过程。

在 BDD 中,无论你写什么都必须进入 给定-何时-然后 步骤。让我们用 BDD 来考虑上面的例子:

Given that a fund transfer module in net banking application has been developed
And I am accessing it with proper authentication

When I shall transfer with enough balance in my source account
Or I shall transfer on a Bank Holiday
Or I shall transfer on a future date
And destination a/c details are correct
And transaction password / rsa code / security authentication for the transaction is correct
And press or click send button

Then amount must be transferred
And the event will be logged in log file

这种表单易于编写、阅读和理解。它涵盖了资金转移模块的所有可能测试用例,并且可以快速修改以适应更多用例。它也像模块的动态文档一样。由于 BDD 源于 TDD,因此在讨论工具之前,有必要先弄清楚两者之间的区别。

BDD 与 TDD:主要区别

这两种实践都采用测试优先的方式,并且都能缩短反馈周期。它们的区别在于谁编写测试、测试使用的语言以及测试描述的是应用程序的哪一层。

方面 TDD(测试驱动开发) BDD(行为驱动开发)
专注 代码单元内部的工作原理 从用户角度看系统的行为方式
语言 编程语言断言 自然语言 Given-When-Then 场景
第一作者 开发商 业务分析师、产品负责人、测试人员和开发人员共同协作。
Readable 由非程序员撰写 没有
典型范围 单位级别 功能、API 和接受程度
常用工具 JUnitpytest、NUnit 表现, CucumberSpecFlow、JBehave

两者是互补的,而不是竞争的。团队通常会保留 TDD 对于单元级设计,可以在此基础上添加 BDD 场景来描述利益相关者实际认可的行为。由于 REST 端点恰好位于验收层,因此它们非常适合 BDD。

什么是 REST API 测试?

随着 REST 成为构建 API 的流行风格,自动化 REST API 测试用例与自动化 UI 测试用例变得同等重要。 API测试 包括分别使用 POST、GET、PUT 和 DELETE 方法测试 CRUD(创建-读取-更新-删除)操作。

Behave 是什么?

Behave 是最受欢迎的 Python BDD 测试框架。以下是 Behave 函数的工作原理:

  • 功能文件由您的业务分析师、发起人或负责行为场景的人员编写。功能文件使用自然语言格式描述功能或功能的一部分,并提供预期结果的代表性示例。
  • 这些场景步骤映射到用以下语言编写的步骤实现: Python.
  • 环境控制程序可以选择性地在步骤、场景、功能或整个运行之前和之后运行代码。

明确了每个文件的作用后,现在可以安装该框架了。

设置 BDD 测试框架 Behave on Windows

安装方式:

项目设置:

  • 创建一个新项目
  • 创建以下目录结构

项目设置

上面的屏幕截图显示了 Behave 期望的布局:a 功能 包含功能文件的目录,以及一个嵌套的 步骤 包含目录 Python 实现方式。Behave 通过名称发现两者,因此文件夹名称必须完全匹配。

功能文件:

现在让我们来构建特征文件 Sample_REST_API_Testing.feature其特点是对“posts”服务执行 CRUD 操作。

这个例子使用了 https://jsonplaceholder.typicode.com/ posts 示例 REST 服务,一个免费的模拟 API,接受写入请求并返回逼真的响应,但不持久化更改。

POST 场景示例

Scenario: POST post example -> creating a new post item using the 'posts' service
Given I set post posts API endpoint          -> prerequisite: sets the URL of the posts service
When I set HEADER param request content type as "application/json"
And set request body
And send POST HTTP request                    -> the actual test step
Then I receive valid HTTP response code 201
And Response body "POST" is non-empty       -> verification of the response body

同样地,其余场景可以写成如下形式:

项目设置

Sample_REST_API_Testing.feature

Feature: Test CRUD methods in Sample REST API testing framework

Background:
    Given I set sample REST API url

Scenario: POST post example
    Given I Set POST posts api endpoint
    When I Set HEADER param request content type as "application/json"
    And Set request Body
    And Send a POST HTTP request
    Then I receive valid HTTP response code 201
    And Response BODY "POST" is non-empty

Scenario: GET posts example
    Given I Set GET posts api endpoint "1"
    When I Set HEADER param request content type as "application/json"
    And Send GET HTTP request
    Then I receive valid HTTP response code 200 for "GET"
    And Response BODY "GET" is non-empty

Scenario: UPDATE posts example
    Given I Set PUT posts api endpoint for "1"
    When I Set Update request Body
    And Send PUT HTTP request
    Then I receive valid HTTP response code 200 for "PUT"
    And Response BODY "PUT" is non-empty

Scenario: DELETE posts example
    Given I Set DELETE posts api endpoint for "1"
    When I Send DELETE HTTP request
    Then I receive valid HTTP response code 200 for "DELETE"

步骤实施

现在,对于上述场景中使用的功能步骤,您可以编写实现。 Python “steps”目录下的文件。

Behave 框架通过将装饰器与特性文件谓词进行匹配来识别步骤函数。例如, 特定 在特征文件场景中,谓词会搜索携带该谓词的阶跃函数。 @given 装饰器也存在同样的匹配情况。 日期然后对于“但是”和“并且”,步骤函数会使用与其前一步相同的装饰器。例如,如果“并且”紧跟在“给定”之后,则匹配的步骤函数装饰器为: @given.

例如,POST 请求的 When 步骤可以按如下方式实现。请注意如何 "application/json" 从功能文件传递到带花括号的占位符中——这称为参数化。

# Decorator: the braced name captures a value from the feature file
@when(u'I Set HEADER param request content type as "{header_content_type}"')
def step_impl(context, header_content_type):
    # Step implementation: set the content type on the request header
    request_headers['Content-Type'] = header_content_type

同样,步骤中其他步骤的实施也类似。 Python 文件将如下所示:

步骤实施

示例步骤实现.py

from behave import given, when, then, step
import requests

api_endpoints = {}
request_headers = {}
response_codes = {}
response_texts = {}
request_bodies = {}
api_url = None

@given(u'I set sample REST API url')
def step_impl(context):
    global api_url
    api_url = 'https://jsonplaceholder.typicode.com'

# START POST Scenario
@given(u'I Set POST posts api endpoint')
def step_impl(context):
    api_endpoints['POST_URL'] = api_url + '/posts'
    print('url :' + api_endpoints['POST_URL'])

@when(u'I Set HEADER param request content type as "{header_content_type}"')
def step_impl(context, header_content_type):
    request_headers['Content-Type'] = header_content_type

# "And" or "But" steps are renamed by behave to match their preceding step
@when(u'Set request Body')
def step_impl(context):
    request_bodies['POST'] = {"title": "foo", "body": "bar", "userId": "1"}

@when(u'Send POST HTTP request')
def step_impl(context):
    # send the request and save the response object
    response = requests.post(url=api_endpoints['POST_URL'],
                             json=request_bodies['POST'],
                             headers=request_headers)
    response_texts['POST'] = response.text
    print("post response :" + response.text)
    response_codes['POST'] = response.status_code

@then(u'I receive valid HTTP response code 201')
def step_impl(context):
    print('Post rep code ;' + str(response_codes['POST']))
    assert response_codes['POST'] == 201
# END POST Scenario

# START GET Scenario
@given(u'I Set GET posts api endpoint "{id}"')
def step_impl(context, id):
    api_endpoints['GET_URL'] = api_url + '/posts/' + id
    print('url :' + api_endpoints['GET_URL'])

@when(u'Send GET HTTP request')
def step_impl(context):
    response = requests.get(url=api_endpoints['GET_URL'], headers=request_headers)
    response_texts['GET'] = response.text
    response_codes['GET'] = response.status_code

@then(u'I receive valid HTTP response code 200 for "{request_name}"')
def step_impl(context, request_name):
    print('Get rep code for ' + request_name + ':' + str(response_codes[request_name]))
    assert response_codes[request_name] == 200

@then(u'Response BODY "{request_name}" is non-empty')
def step_impl(context, request_name):
    print('request_name: ' + request_name)
    print(response_texts)
    assert response_texts[request_name] is not None
# END GET Scenario

# START PUT/UPDATE
@given(u'I Set PUT posts api endpoint for "{id}"')
def step_impl(context, id):
    api_endpoints['PUT_URL'] = api_url + '/posts/' + id
    print('url :' + api_endpoints['PUT_URL'])

@when(u'I Set Update request Body')
def step_impl(context):
    request_bodies['PUT'] = {"title": "foo", "body": "bar",
                             "userId": "1", "id": "1"}

@when(u'Send PUT HTTP request')
def step_impl(context):
    response = requests.put(url=api_endpoints['PUT_URL'],
                            json=request_bodies['PUT'],
                            headers=request_headers)
    response_texts['PUT'] = response.text
    print("update response :" + response.text)
    response_codes['PUT'] = response.status_code
# END PUT/UPDATE

# START DELETE
@given(u'I Set DELETE posts api endpoint for "{id}"')
def step_impl(context, id):
    api_endpoints['DELETE_URL'] = api_url + '/posts/' + id
    print('url :' + api_endpoints['DELETE_URL'])

@when(u'I Send DELETE HTTP request')
def step_impl(context):
    response = requests.delete(url=api_endpoints['DELETE_URL'])
    response_texts['DELETE'] = response.text
    print("DELETE response :" + response.text)
    response_codes['DELETE'] = response.status_code
# END DELETE

⚠️ 注意: 将状态码与 == 而非 is 事情。 is 运算符测试对象的身份,而不是相等性,并且 Python 3.8 及之后提出 SyntaxWarning: “is” 与字面量一起使用 对于该模式。任何断言都写成这样 assert code is 201 应改写为 assert code == 201.

运行测试

测试脚本开发已完成,现在让我们运行测试。在命令提示符下执行以下命令来运行功能文件:

:: Run a single feature file with the pretty formatter
behave -f pretty features\feature_files_folder\Sample_REST_API_Testing.feature

:: Run every feature in the project
behave

测试执行结果显示如下:

运行测试

控制台上显示报告

控制台输出在开发过程中很方便,但利益相关者通常更喜欢易于阅读的报告,而 Allure 正好提供了这种报告。

报告

首先,安装 Allure Behave 格式化程序和 Allure 命令行工具。该格式化程序是一个 Python 软件包;命令行工具是单独下载的,详情请参见…… Allure报告文档.

:: 1. Install the formatter
pip install allure-behave

:: 2. Run the tests and write raw results to a folder
behave -f allure_behave.formatter:AllureFormatter -o reports/allure-results features/

:: 3. Render and open the HTML report
allure serve reports/allure-results

这将生成如下所示的、易于理解且信息丰富的测试结果报告:

报告

HTML 格式的测试报告

HTML 格式的测试报告

测试报告显示个别场景结果

一个功能套件只有在 API 不断增长的情况下仍然保持可读性时才有用,而这正是功能文件规范发挥作用的地方。

编写 Behave 功能文件的最佳实践

当场景描述的是点击和有效负载而不是行为时,Behave 套件的性能会迅速下降。以下实践可确保功能文件对业务利益相关者易于阅读,同时保持……ping 这个 Python 可供工程师维护的层。

  1. 描述行为,而不是实现方式。 请写成“假设客户账户余额充足”,而不是“假设余额列等于 500”。功能文件描述了系统执行的操作;步骤文件描述了如何进行检查。
  2. 每个场景只保留一种行为。 一个包含状态码、响应体和数据库行的场景实际上包含了三个场景。将它们拆分开来,就能让故障直接指向原因。
  3. 将共享设置移至后台运行。 上面的例子使用了 Background: Given I set sample REST API url 因此每个场景都继承了基础 URL 无需重复。
  4. 使用参数化代替重复。 带括号的占位符,例如 "{request_name}" 让一个步骤函数来处理 GET、PUT 和 DELETE 断言,这就是为什么示例需要的步骤定义比场景少得多。
  5. 使用场景大纲来描述数据变化。 当需要证明同一行为对多个输入都适用时, Examples: 表格比复制的场景描述更清晰。
  6. 避免场景间依赖关系。 Behave 并不保证在每种配置下 POST 请求都会在 GET 请求之前执行。每个请求都应该创建其所需的状态。
  7. 将模块级全局变量替换为上下文。 为了简洁起见,示例中将端点和响应存储在模块字典中。在生产环境中,应将它们存储在 Behave 的 context 对象状态可以在不同场景之间干净利落地重置。
  8. 针对特定运行情况的标签场景。 例如标签 @smoke or @regressionbehave --tags=@smoke这样可以保持管道反馈快速。

从第一个特性文件开始就应用这些习惯,可以使 BDD 套件的价值远远超出最初的 CRUD 示例。进一步扩展这种方法的团队通常会将其与以下方法结合使用: Cucumber 对于 JVM 项目或 Postman 用于探索性 API 检查。

常见问题

两者都读取 Gherkin 特征文件。 Cucumber 将步骤绑定到 JavaRuby,或 Java脚本,而行为则将它们绑定到 Python选择与您的应用程序堆栈相匹配的方案,以便步骤定义可以重用现有的测试工具。

是的。当任何场景失败时,Behave 函数都会返回非零退出代码,因此 JenkinsGitLab CI 或 GitHub Actions 可能会自动导致构建失败。请将 Allure 结果文件夹作为构建产物发布,以便生成报告。

是的。 AI 助手根据用户故事或 OpenAPI 规范起草 Given-When-Then 场景。 Rev查看缺失的负面案例和业务规则的输出,因为生成的场景通常只涵盖正常情况。

AI 工具能够检测重复或近似重复的场景,建议可重用的参数化步骤,并根据可能的原因对不稳定的故障进行聚类。随着团队间功能文件数量的增加,这可以减少人工审核工作量。

在 environment.py 的 before_all 钩子函数中获取一次令牌,将其存储在上下文对象中,并在每个请求步骤中将其作为 Authorization 标头附加。将凭据保存在环境变量中,切勿保存在功能文件中。

总结一下这篇文章: