跳转到主要内容

BDD 行为驱动开发测试

项目描述

behaving

GitHub Workflow Status PyPI Docker Image Version (latest by date)

behaving 是一个基于 behavesplinter 的行为驱动开发(BDD)测试框架。

behaving 使用Python编写,类似于 Cucumberbehaving 添加了用于多用户Web/电子邮件/SMS/GCM交互的步骤库,并提供Python的 behaving 命名空间,以便独立的步骤库可以协同工作。

请参阅 behave 的优秀 文档,了解如何使用它,如何编写自定义步骤以及如何扩展 behaving

你好世界

开始使用 behaving 非常简单。在某个Python模块中,添加你的 features,每个 feature 包含一个或多个场景。这些 features 是扩展名为 .feature 的Gherkin语言文件。在同一目录中,你应该有一个步骤模块,它导入 behaving 步骤以及你的自定义步骤(有关设置部分的更多内容将在后面介绍)。以下是一个基本示例

Feature: Text presence

    Background:
        Given a browser

    Scenario: Search for BDD
        When I visit "http://www.wikipedia.org/"
        And I fill in "search" with "BDD"
        And I press "go"
        Then I should see "Behavior-driven development" within 5 seconds

电子邮件,短信 & GCM(Google Cloud Messaging)

虽然 behaving 的重点是Web,但它还包括对邮件、短信和GCM服务器的简单模拟。这些模拟附带了一组步骤,允许你执行以下操作

Feature: Email & SMS

    Scenario: Click link in an email
        Given a browser
        When I send an email to "foo@bar.com" with subject "Hello" and body "Try out this website at http://google.com"
        And I click the link in the email I received at "foo@bar.com"
        Then the browser's URL should be "http://google.com/"

    Scenario: Receive SMS with body
        When I send an sms to "+4745690001" with body "Hello world"
        Then I should receive an sms at "+4745690001" containing "world"

    Scenario: Receive GCM Notification
        When I send a gcm message "{"to":"deviceID", "data": {"message": "Foo Bar", "badge": 6}}"
        Then I should receive a gcm notification at "deviceID" containing "{'data': {'message': 'Foo Bar'}}"

通常,发送电子邮件/短信/通知的是您的网络应用程序,测试它主要涉及到配置应用程序向模拟服务器发送电子邮件/短信/通知。

人物 & 状态

如今,许多网络应用程序都依赖于多用户交互。为了帮助您处理这些交互,behaving 使用了 人物 的概念。测试中的一个人物在其自己的浏览器实例中运行,您可以有多个同时运行的人物(及其浏览器实例)。您可以通过调用以下方式在人物之间切换

Given "PersonaName" as the persona

人物通常也作为简单的字典实现,允许它们携带状态,在场景内保存和重用变量。当人物首次调用时,它被创建为一个空字典。您可以通过设置预定义的人物值。

让我们以熟悉的中土世界角色作为测试用户。在设置测试环境(详情见设置_部分)时,我们将基本变量设置为测试中可能需要的,如下所示

PERSONAS = {
    'Frodo': dict(
        fullname=u'Frodo Baggins',
        email=u'frodo@shire.com',
        password=u'frodopass',
        mobile='+4745690001',
        address: {
            street: "The Shire",
            zip: "4321"
        }
    ),
    'Gandalf': dict(
        fullname=u'Gandalf the Grey',
        email=u'gandalf@wizardry.com',
        password=u'gandalfpass',
        mobile='+4745690004',
        address: {
            street: "Rivendell street 1",
            zip: "1234"
        }
  ),
  ...
}
def before_scenario(context, scenario):
    ...
    context.personas = PERSONAS

在测试中,给定一个人物,您现在可以使用 $var_name 来访问人物的变量。您也可以在人物上设置新的变量。所以以下内容

Given "Gandalf" as the persona
When I fill in "name" with "$fullname"
And I fill in "street" with "$address.street"
And I set "title" to the text of "document-title"
And I fill in "delete" with "$title"
And I set "address.country" to the text of "country"
And I set "postaddress" to:
"""
$fullname
$address.street, $address.zip, $address.country
"""

将填写具有 id name 的字段,为 Gandalf the Grey,为 street 设置 Rivendell street 1,将变量 title 设置为具有 id document-title 的元素的文本,并重用变量 title 来填写具有 id delete 的字段。它还会将具有 id "country" 的字段的值存储在 address[country] 中。$var_name 模式也可以用于期望文本体的步骤收到的文本中,这意味着 postaddress 人物变量将包含 Gandalf 的完整蜗牛邮件邮寄地址,格式良好地显示在多行中。

Hello Persona 示例

让我们假设以下场景(来自真实示例)。Crypho 是一个在线消息/共享网站,为用户提供端到端加密的实时通信。为了测试 Crypho,编写了 behaving

Crypho 中,团队在 空间 中协作。要邀请某人加入一个 空间,邀请人必须与邀请者共享一个令牌,以便双方可以验证对方的身份。

Feature: Frodo invites Gandalf to The Shire space

    Given state "the-shire"

    Scenario: Frodo invites Gandalf to The Shire

        Given "Gandalf" as the persona
        When I log in

在场景开始之前,自定义步骤 Given state "the-shire" 执行。这预先加载了 db 中的数据,设置了服务器等。然后执行场景

首先,Gandalf 登录。步骤 Given "Gandalf" as the persona 启动属于人物 Gandalf 的浏览器。以下步骤 When I log in 是自定义步骤,如下定义

@when('I log in')
def log_in(context):

    assert context.persona
    context.execute_steps(u"""
        When I go to Home
            Then I should see an element with id "email" within 2 seconds
        When I fill in "email" with "$email"
        And I press "send-sms"
            Then I should see "We have sent you an SMS with a security code" within 2 seconds
            And I should receive an sms at "$mobile"
            And "token" should be enabled
        When I parse the sms I received at "$mobile" and set "Your Crypho code is {token}"
        And I fill in "token" with "$token"
        And I fill in "password" with "$password"
        And I press "login"
            Then I should see "Crypho" within 5 seconds
    """)

观察上面当前人物(Gandalf)如何解析收到的短信并将其保存为 "token"。后来,Gandalf 重新使用它来填写双因素认证字段。

现在 Gandalf 已登录,测试继续进行到 Frodo。Frodo 将登录,并邀请 Gandalf 加入一个私人空间。

Given "Frodo" as the persona
When I log in
And I click the link with text that contains "My spaces"
And I click the link with text that contains "The Shire"
And I press "invite-members"
    Then I should see "Invite members" within 1 seconds
When I fill in "invitees" with "gandalf@wizardry.com"
And I fill in "invitation-message" with "Come and join us!"
And I press "send-invitations"
    Then I should see "Your invitations have been sent" within 2 seconds

发送邀请后,我们切换回 Gandalf 的浏览器,他应该在他的浏览器中收到通知,以及一封电子邮件。然后他继续向 Frodo 发送包含令牌的短信,Frodo 完成邀请。

Given "Gandalf" as the persona
Then I should see "Your invitations have been updated" within 2 seconds
And I should receive an email at "gandalf@wizardry.com" containing "Frodo Baggins has invited you to join a private workspace in Crypho"
When I click the link with text that contains "Invitations"
And I click the link with text that contains "Pending invitations"
    Then I should see "Come and join us!"
When I set "token" to the text of "invitation-token"
And I send an sms to "45699900" with body "$token"

Given "Frodo" as the persona
    Then I should receive an sms at "45699900"
When I set "FrodoToken" to the body of the sms I received at "45699900"
And I click the link with text that contains "Invitations"
And I click the link with text that contains "Enter authorization token"
And I fill in "auth-token" with "$FrodoToken"
And I press "Submit"
    Then I should see "The invitation has been accepted." within 5 seconds
    And I should see "Gandalf the Grey has joined the space, invited by Frodo Baggins" within 10 seconds

您可以在以下视频 此处 看到测试的实际操作。

设置测试环境

首先,使用 pipeasy_install 安装 behaving。这将安装依赖项并创建 behave 脚本,您可以用它来调用测试。如果您更喜欢使用 buildout,从其存储库克隆包本身,它已经包含一个 buildout 配置。

通常,您将有一个包含所有功能步骤的文件夹。例如,以下目录结构

features/
features/mytest.feature
features/myothertest.feature
features/environment.py
features/steps/
features/steps/steps.py

在步骤目录中,您需要导入所需的 behaving 步骤。您也可以定义自己的步骤。因此,steps.py 可能看起来像

from behave import when
from behaving.web.steps import *
from behaving.sms.steps import *
from behaving.mail.steps import *
from behaving.notifications.gcm.steps import *
from behaving.personas.steps import *

@when('I go to home')
def go_to_home(context):
    context.browser.visit('https://web/')

environment.py 中,您可以指定设置以及需要在测试各个阶段发生的事情,例如在所有事情之前和之后、一个功能运行或一个场景运行之前。为了方便起见,您可以导入并重用 behaving.environment,它将执行默认操作,如场景结束后关闭所有浏览器,清理电子邮件文件夹等。

如果您不需要短信功能,也可以单独使用 behaving.web.environmentbehaving.mail.environmentbehaving.sms.environmentbehaving.personas.environment

以下是一个示例环境,它仅设置一些变量,然后依赖默认操作来完成各个阶段的操作:

import os
from behaving import environment as benv

PERSONAS = {}

def before_all(context):
    import mypackage
    context.attachment_dir = os.path.join(os.path.dirname(mypackage.__file__), 'tests/data')
    context.sms_path = os.path.join(os.path.dirname(mypackage.__file__), '../../var/sms/')
    context.gcm_path = os.path.join(os.path.dirname(mypackage.__file__), '../../var/gcm/')
    context.mail_path = os.path.join(os.path.dirname(mypackage.__file__), '../../var/mail/')
    benv.before_all(context)


def after_all(context):
    benv.after_all(context)


def before_feature(context, feature):
    benv.before_feature(context, feature)


def after_feature(context, feature):
    benv.after_feature(context, feature)


def before_scenario(context, scenario):
    benv.before_scenario(context, scenario)
    context.personas = PERSONAS

def after_scenario(context, scenario):
    benv.after_scenario(context, scenario)

以下变量受支持,并且可以设置以覆盖默认值:

  • screenshots_dir(屏幕截图将保存的路径。如果设置,则场景中的任何失败都将导致在失败发生时浏览器屏幕的截图。)
  • attachment_dir(文件附件可以找到的路径)
  • sms_path(smsmock 保存短信的路径。默认为 current_dir/sms
  • gcm_path(gcmmock 保存 gcm 通知的路径。默认为 current_dir/gcm
  • mail_path(mailmock 保存邮件的路径。默认为 current_dir/mail
  • default_browser
  • default_browser_size(元组(宽度,高度),应用于创建的每个浏览器)
  • max_browser_attempts(在浏览器创建失败时重试创建浏览器的次数)
  • remote_webdriver_url(指向您的 selenium hub url 或远程 webdriver。默认为 None
  • browser_args(在创建浏览器时使用的额外关键字参数的字典)
  • base_url(浏览器的基 url,允许您使用相对路径)
  • accept_ssl_certs(设置为 True 将接受自签名/无效证书。默认为 None

您可以通过运行以下命令来执行测试:

./bin/behave ./features

对于 chrome 和 docker 问题,以下代码很有用

from selenium.webdriver.chrome.options import Options
chrome_options = Options()
chrome_options.add_argument('--no-sandbox')
context.browser_args = {
    'options': chrome_options
}

邮件、GCM 和 SMS 模拟服务器

当安装 behaving 时,它创建三个脚本来帮助您测试邮件、gcm 和短信,分别是 mailmockgcmmocksmsmock。您可以直接在运行测试之前调用它们,它们都接受一个端口号以及输出数据的目录作为参数。例如,

./bin/smsmock -p 8081 -o ./var/sms
./bin/gcmmock -p 8082 -o ./var/notifications/gcm
./bin/mailmock -p 8083 -o ./var/mail [--no-stdout]

behaving.web 支持的匹配器/步骤

  • 浏览器

    • 给定一个浏览器 [打开默认浏览器,即 Firefox]
    • 给定 brand 作为默认浏览器 [将默认浏览器设置为 brand,这是在远程 webdriver 或 Firefox、Chrome、Safari 中使用的浏览器名称]
    • 给定电子应用程序 "app_path" [用于基于电子的桌面应用程序]
    • 给定浏览器 "name" [打开名为 name 的浏览器]
    • 当我重新加载
    • 当我后退
    • 当我前进
    • 当我将浏览器调整到 widthxheight
    • 当我调整视口大小为 widthxheight
    • 当我截图 [如果环境中的 screenshots_dir 已设置,则将保存浏览器截图。如果设置了 screenshots_dir,则所有失败的测试都将产生截图。]
    • 当我执行脚本 "script"
    • 当我将 cookie "key" 设置为 "value"
    • 当我删除 cookie "key"
    • 当我删除所有 cookie
    • 当我关闭浏览器 "name"
  • 框架

    • 当我切换到具有 css "css" 的框架
    • 当我切换回主页
  • 窗口

    • 当我打开一个名为 "name" 的窗口,地址为 "url" 时
    • 当我将当前窗口命名为 "name"
    • 当我切换到名为 "name" 的窗口
  • URLs

    • 给定基本 URL "url" [设置基本 URL 为 url,或者在 environment.py 中直接设置 context.base_url]
    • 当我访问 "url"
    • 当我转到 "url"
    • 当我解析 URL 路径并设置 "{expression}"
    • 那么浏览器的 URL 应该是 "url"
    • 那么浏览器的 URL 应该包含 "text"
    • 那么浏览器的 URL 不应该包含 "text"
  • 链接

    • 当我点击链接到 "url"
    • 当我点击链接到包含 "url" 的 URL
    • 当我点击文本为 "text" 的链接
    • 当我点击文本包含 "text" 的链接
  • 文本、元素 & 类存在

    • 当我等待 timeout

    • 当我显示 id 为 "id" 的元素

    • 当我隐藏 id 为 "id" 的元素

    • 文本

      • 那么我应该看到 "text"
      • 那么我不应该看到 "text"
      • 那么我应该看到 "text",在 timeout 秒内
      • 那么我不应该看到 "text",在 timeout 秒内
    • ID

      • 那么我应该看到一个 id 为 "id" 的元素
      • 那么我不应该看到一个 id 为 "id" 的元素
      • 那么我应该看到一个 id 为 "id" 的元素,在 timeout 秒内
      • 那么我不应该看到一个 id 为 "id" 的元素,在 timeout 秒内
  • CSS

    • 存在
      • 那么我应该看到一个具有 CSS 选择器 "selector" 的元素
      • 那么我不应该看到一个具有 CSS 选择器 "selector" 的元素
      • 那么我应该看到一个具有 CSS 选择器 "selector" 的元素,在 timeout 秒内
      • 那么我不应该看到一个具有 CSS 选择器 "selector" 的元素,在 timeout 秒内
      • 那么我应该看到 n 个具有 CSS 选择器 "css" 的元素
      • 那么我应该看到至少 n 个具有 CSS 选择器 "css" 的元素,在 timeout 秒内
    • 可见性
      • 那么具有 CSS 选择器 "css" 的元素应该是可见的
      • 那么具有 CSS 选择器 "css" 的元素应该在 timeout 秒内可见
      • 那么具有 CSS 选择器 "css" 的元素不应该可见
      • 那么具有 CSS 选择器 "css" 的元素应该在 timeout 秒内可见
      • 那么应该有 {n:d} 个具有 CSS 选择器 "css" 的元素是可见的
      • 那么应该有 {n:d} 个具有 CSS 选择器 "css" 的元素在 timeout 秒内是可见的
      • 那么至少应该有 {n:d} 个具有 CSS 选择器 "css" 的元素是可见的
      • 那么至少应该有 {n:d} 个具有 CSS 选择器 "css" 的元素在 timeout 秒内是可见的
    • 元素上的类存在
      • 那么具有 xpath "xpath" 的元素应该有类 "cls"
      • 那么具有 xpath "xpath" 的元素不应该有类 "cls"
      • 那么具有 xpath "xpath" 的元素应该在 timeout 秒内有类 "cls"
      • 那么具有 xpath "xpath" 的元素应该在 timeout 秒内没有类 "cls"
      • 那么 "name" 应该有类 "cls"
      • 那么 "name" 不应该有类 "cls"
      • 那么 "name" 应该在 timeout 秒内有类 "cls"
      • 那么 "name" 应该在 timeout:d 秒内没有类 "cls"
    • XPath
      • 那么我应该看到一个具有 xpath "xpath" 的元素
      • 那么我不应该看到一个具有 xpath "xpath" 的元素
      • 那么我应该看到一个具有 xpath "xpath" 的元素,在 timeout 秒内
      • timeout 秒内,我不应看到一个具有 xpath "xpath" 的元素
  • 表单

    • 当我在 "name|id" 中填写 "value"
    • 当我在 "name|id" 中清除字段
    • 当我将 "value" 键入 "name|id" 中 [与填充相同,但发生较慢,触发键盘事件]
    • 当我从 "name" 中选择 "value"
    • 当我选择 "name|id"
    • 当我取消选择 "name|id"
    • 当我切换 "name|id"
    • 当我从 "name" 中选择 "value"
    • 当我按文本 "text" 从 "name" 中选择
    • 当我按下 "name|id|text|innerText"
    • 当我按下具有 xpath "xpath" 的元素
    • 当我将文件 "path"附加到 "name" 上
    • 当我将具有 id "id" 的元素的内部 HTML 设置为 "contents" [将具有 id "id" 的 contenteditable 元素的 HTML 设置为 "contents"]
    • 当我将具有 class "class" 的元素的内部 HTML 设置为 "contents"
    • 当我将具有 class "class" 的元素的内部 HTML 设置为 "contents"
    • 当我将 "KEY" 发送到 "name"
    • 当我在 "name" 上获得焦点
    • 然后字段 "name" 应具有值 "value"
    • 然后字段 "name" 应具有值 "value" 在 timeout 秒内
    • 然后选择 "name" 应具有选项 "valueA, valueB" 被选中
    • 然后 "name" 应启用
    • 然后 "name" 应禁用
    • 然后 "name" 不应启用
    • 然后 "name" 应有效
    • 然后 "name" 应无效
    • 然后 "name" 不应有效
    • 然后 "name" 应为必填项
    • 然后 "name" 不应为必填项
  • HTML 表格

    • 然后具有 id "id" 的表应该是
      | 表头1 | 表头2 | ... | 表头(m) |
      | 单元格00 | 单元格01 | ... | 单元格0m |
      | 单元格10 | 单元格11 | ... | 单元格1m |
      ...
      | 单元格n0 | 单元格n1 | ... | 单元格nm |

    • 然后具有 xpath "xpath" 的表应该是
      | 表头1 | 表头2 | ... | 表头(m) |
      | 单元格00 | 单元格01 | ... | 单元格0m |
      | 单元格10 | 单元格11 | ... | 单元格1m |
      ...
      | 单元格n0 | 单元格n1 | ... | 单元格nm |

    • 然后具有 id "id" 的表应包含行
      | 单元格00 | 单元格01 | ... | 单元格0m |
      | 单元格10 | 单元格11 | ... | 单元格1m |

    • 然后具有 xpath "xpath" 的表应包含行
      | 单元格00 | 单元格01 | ... | 单元格0m |
      | 单元格10 | 单元格11 | ... | 单元格1m |

    • 然后具有 id "id" 的表不应包含行
      | 单元格00 | 单元格01 | ... | 单元格0m |
      | 单元格10 | 单元格11 | ... | 单元格1m |

    • 然后具有 xpath "xpath" 的表不应包含行
      | 单元格00 | 单元格01 | ... | 单元格0m |
      | 单元格10 | 单元格11 | ... | 单元格1m |

    • 然后表格中行 row_no 应该是
      | 单元格00 | 单元格01 | ... | 单元格0m |

    • 然后表格中行 row_no 应该是
      | 单元格00 | 单元格01 | ... | 单元格0m |

    • 然后表格中行 row_no,列 col_no 的值应该是 "value"

    • 然后表格中行 row_no,列 col_no 的值应该是 "value"

    • 然后表格中行 row_no,列 "col_header" 的值应该是 "value"

    • 然后表格中行 row_no,列 "col_header" 的值应该是 "value"

  • 警报 & 提示

    • 当我将 "text" 输入到警报中 - 当我接受警报 - 当我关闭警报 - 然后我应该看到警报 - 然后我应该看到警报在 timeout 秒内 - 然后我应该看到包含 "text" 的警报 - 然后我应该看到在 timeout 秒内包含 "text" 的警报
  • 鼠标

    • 当我将鼠标悬停在具有 xpath "xpath" 的元素上
    • 当我将鼠标移出具有 xpath "xpath" 的元素
  • 下载

    • 然后文件 "filename" 应在 timeout 秒内下载
    • 然后文件 "filename" 应该在 timeout 秒内已下载
  • Persona交互 & 变量

    • 当我将 "key" 设置为 "id|name" 的文本时
    • 当我将 "key" 设置为具有 xpath "xpath" 的元素的属性 "attr" 时
    • 当我评估脚本 "script" 并将结果分配给 "key" 时

behaving.mail 支持的匹配器/步骤

  • 当我在 "address" 收到的电子邮件中点击链接时
  • 当我在 "address" 收到的电子邮件中解析并设置 "expression" 时
  • 当我清除电子邮件消息时
  • 然后我应该收到 "address" 的电子邮件
  • 然后我应该收到主题为 "subject" 的 "address" 电子邮件
  • 然后我应该收到包含 "text" 的 "address" 电子邮件
  • 然后我应该收到包含 "filename" 附件的 "address" 电子邮件
  • 然后我应该没有在 "address" 收到任何电子邮件

behaving.sms 支持的匹配器/步骤

  • 当我在 "number" 收到的短信正文中设置 "key" 时
  • 当我在 "number" 收到的短信中解析并设置 "expression" 时
  • 然后我应该收到 "number" 的短信
  • 然后我应该收到包含 "text" 的 "number" 短信

behaving.notifications.gcm 支持的匹配器/步骤

  • 当我发送 gcm 消息 "{"to":"deviceID", "data": {"message":"Foo Bar", "badge": 6}}"
  • 然后我应该收到包含 "{'data': {'message': 'Foo Bar'}}" 的 "deviceID" gcm 通知
  • 然后我应该收到 "deviceID" 的任何 gcm 通知

behaving.personas 支持的匹配器/步骤

  • 给定 "name" 作为 persona
  • 当我将 "key" 设置为 "value" 时
  • 当我将 "key" 设置为
    """ some longer body of text
    通常是多行
    """
  • 当我将 persona "source" 克隆到 "target" 时
  • 然后 "key" 被设置为 "value"

调试

  • 当我在测试中暂停时

Docker 集成

提供了一个 Dockerfile 以及使用 docker-compose 的完整设置,以帮助您创建运行测试的 selenium grid 配置。此外,如果 VSCode 是您的事情,还包含 dev 容器配置。

此外,我们还在 docker hub 上为 linux/amd64linux/arm64 平台提供了预构建的镜像。使用

docker pull behaving/behaving:latest

来拉取镜像。

运行 behaving 测试

您可以通过以下方式运行所有 behaving 测试

启动 docker compose

docker-compose up

在 behaving 容器中打开一个 shell

docker-compose exec behaving bash

运行 behaving 测试

behave tests/features

项目详情


下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。

源分布

behaving-3.1.5.tar.gz (48.1 kB 查看哈希值)

上传时间

构建分布

behaving-3.1.5-py3-none-any.whl (51.4 kB 查看哈希值)

上传时间 Python 3