BDD中的step可复用性调研

Posted by Shi Hai's Blog on August 22, 2023

一、背景介绍

BDD开发测试用例一定时间后,give\when\then语句会越来越多,这会导致BDD用例描述持续恶化,主要表现在:

  • 开发人员的思维和表达方式不同会导致多个语句表达一种行为;
  • 没有复用组合语句会导致测试点越多,行为描述也会越多;

本文主要尝试从业内在BDD的应用重点看一下第二个问题是否有更好的解法或者组织形式。

二、可行性分析

两种维度的复用策略没有哪种更加优秀,而是需要考虑面向不同的群体,采取哪种策略的才能更好的发挥BDD的潜能。

2.1 given\when\then的复用

2.1.1 Chorus BDD

Chrous在Step Macro中描述了一种行为复用的逻辑,这种逻辑能让行为和实现继续分层管理,从而不让技术实现来破坏行为描述的完整性,具体的示例如下所示:

#file: login.stepmacro
Step-Macro: I log in as user <user> with password <password>
    Given I click the login button
    And the login dialog is shown
    When I type <user> into the field username
    And I type <password> into the field password
    And I click the OK button

#file: login.feature
Feature: Log In 

    Scenario: I can log in using the login form
        Given I log in as user Nick with password myPassword
        Then I am logged in as the user Nick
    
#file: accountSummary.feature
Feature: Account Summary
 
    Scenario: Account summary link is shown when logged in
        Given I log in as user Nick with password myPassword
        Then the account summary link is visible on homepage

2.2 step实现函数的复用

2.2.1 behave

和Chorus Bdd比较而言,behave则提供了另外一种step复用的方式,那就是通过定义Step Macro来使得step函数实现能进行组合复用,通过调用context.execute_steps()就能调用不同的step函数。

@when(u'I do the same thing as before with the {color:w} button')
def step_impl(context, color):
    context.execute_steps(u'''
        When I press the big {color} button
         And I duck
    '''.format(color=color))

2.2.2 pytest-bdd

实际pytest-bdd本身就支持不同step的调用,这个调用没有用任何技术技巧,而是通过调用函数就可以实现,因为step装饰器没有对函数做任何限制。

def step(
    name: str | StepParser,
    type_: Literal["given", "when", "then"] | None = None,
    converters: dict[str, Callable] | None = None,
    target_fixture: str | None = None,
    stacklevel: int = 1,
) -> Callable[[TCallable], TCallable]:
    """Generic step decorator."""
    if converters is None:
        converters = {}

    def decorator(func: TCallable) -> TCallable:
        parser = get_parser(name)

        context = StepFunctionContext(
            type=type_,
            step_func=func,
            parser=parser,
            converters=converters,
            target_fixture=target_fixture,
        )
        ...
        return func

    return decorator

这样是否可行?当然从技术维度讲有实现的办法,但是个人不太建议这么做,因为测试函数本身直接调用函数就已经完全丢失了BDD中B的内容。
另外一个不建议的技术原因是pytest是通过大量fixture组织起来的,如果直接调用fixture函数就会发现会提示无法直接调用,会报如下所示的错误:

Failed: Fixture "fixture_name" called directly. Fixtures are not meant to be called directly
def wrap_function_to_error_out_if_called_directly(
    function: FixtureFunction,
    fixture_marker: "FixtureFunctionMarker",
) -> FixtureFunction:
    """Wrap the given fixture function so we can raise an error about it being called directly,
    instead of used as an argument in a test function."""
    message = (
        'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
        "but are created automatically when test functions request them as parameters.\n"
        "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n"
        "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code."
    ).format(name=fixture_marker.name or function.__name__)

    @functools.wraps(function)
    def result(*args, **kwargs):
        fail(message, pytrace=False)

    # Keep reference to the original function in our own custom attribute so we don't unwrap
    # further than this point and lose useful wrappings like @mock.patch (#3774).
    result.__pytest_wrapped__ = _PytestWrapper(function)  # type: ignore[attr-defined]

    return cast(FixtureFunction, result)

关于pytest的几个核心类可以看这篇文章

三、技术挑战

3.1 Gherkin生态支持

这个办法属于非Gherkin标准定义语言,无法兼容生态标准。 这个10年前的discussion中已经有些讨论了,结论是**Gherkin是一个合作工具而不是一门计算机语言**。因为这个议题是10年前的讨论过程,近10年的DSL及IDE发展是非常快的,10年后还是坚持10年前的讨论结论不一定合适。讨论详情

3.2 IDEA-Pycharm

在专业版的Pycharm中,Gherkin解释器是内置插件,没有办法修改源码。可以通过Gherkin语言定义的演进从而使得下游生态适配调整。

3.3 IDEA-Intellij

Intellij的部分插件是开源的,cucumber插件,应该可以进行侵入修改。

参考文献