在 Python 行为测试框架中处理异常

Handling Exceptions in Python Behave Testing framework

我一直在考虑从鼻子切换到行为测试(mocha/chai 等把我宠坏了)。到目前为止一切顺利,但我似乎无法找出任何测试异常的方法:

@then("It throws a KeyError exception")
def step_impl(context):
try:
    konfigure.load_env_mapping("baz", context.configs)
except KeyError, e:
    assert (e.message == "No baz configuration found") 

有了鼻子,我可以用

注释测试
@raises(KeyError)

我在行为中找不到这样的东西(不在源代码中,不在示例中,不在此处)。如果能够在场景大纲中指定可能抛出的异常,那就太好了。

有人走过这条路吗?

我自己对 BDD 很陌生,但一般来说,想法是测试记录客户可以期望的行为 - 而不是步骤实现。所以我希望测试这个的规范方法是这样的:

When I try to load config baz
Then it throws a KeyError with message "No baz configuration found"

步骤定义如下:

@when('...')
def step(context):
    try:
        # do some loading here
        context.exc = None
    except Exception, e:
        context.exc = e

@then('it throws a {type} with message "{msg}"')
def step(context, type, msg):
    assert isinstance(context.exc, eval(type)), "Invalid exception - expected " + type
    assert context.exc.message == msg, "Invalid message - expected " + msg

如果这是一个常见的模式,您可以编写自己的装饰器:

def catch_all(func):
    def wrapper(context, *args, **kwargs):
        try:
            func(context, *args, **kwargs)
            context.exc = None
        except Exception, e:
            context.exc = e

    return wrapper

@when('... ...')
@catch_all
def step(context):
    # do some loading here - same as before

Behave 不属于断言匹配器业务。因此,它没有为此提供解决方案。已经有足够的 Python 包解决了这个问题。

另请参见: behave.example: Select an assertion matcher library

Barry 的这种 try/catch 方法有效,但我发现了一些问题:

  • 在您的步骤中添加 try/except 意味着错误将被隐藏。
  • 添加一个额外的装饰器是不优雅的。我希望我的装饰器是经过修改的 @where

我的建议是

  • 在失败语句之前有 expect 异常
  • 在 try/catch 中,如果错误不是预期的则提出
  • 在 after_scenario 中,如果未找到预期的错误,则引发错误。
  • 到处使用修改后的given/when/then

代码:

    def given(regexp):
        return _wrapped_step(behave.given, regexp)  #pylint: disable=no-member

    def then(regexp):
        return _wrapped_step(behave.then, regexp)  #pylint: disable=no-member

    def when(regexp):
        return _wrapped_step(behave.when, regexp) #pylint: disable=no-member


    def _wrapped_step(step_function, regexp):
        def wrapper(func):
            """
            This corresponds to, for step_function=given

            @given(regexp)
            @accept_expected_exception
            def a_given_step_function(context, ...
            """
            return step_function(regexp)(_accept_expected_exception(func))
        return wrapper


    def _accept_expected_exception(func):
        """
        If an error is expected, check if it matches the error.
        Otherwise raise it again.
        """
        def wrapper(context, *args, **kwargs):
            try:
                func(context, *args, **kwargs)
            except Exception, e:  #pylint: disable=W0703
                expected_fail = context.expected_fail
                # Reset expected fail, only try matching once.
                context.expected_fail = None
                if expected_fail:
                    expected_fail.assert_exception(e)
                else:
                    raise
        return wrapper


    class ErrorExpected(object):
        def __init__(self, message):
            self.message = message

        def get_message_from_exception(self, exception):
            return str(exception)

        def assert_exception(self, exception):
            actual_msg = self.get_message_from_exception(exception)
            assert self.message == actual_msg, self.failmessage(exception)
        def failmessage(self, exception):
            msg = "Not getting expected error: {0}\nInstead got{1}"
            msg = msg.format(self.message, self.get_message_from_exception(exception))
            return msg


    @given('the next step shall fail with')
    def expect_fail(context):
        if context.expected_fail:
            msg = 'Already expecting failure:\n  {0}'.format(context.expected_fail.message)
            context.expected_fail = None
            util.show_gherkin_error(msg)
        context.expected_fail = ErrorExpected(context.text)

我导入修改后的 given/then/when 而不是行为,并添加到我的 environment.py 启动 context.expected 场景之前失败并在之后检查它:

    def after_scenario(context, scenario):
        if context.expected_fail:
            msg = "Expected failure not found: %s" % (context.expected_fail.message)
            util.show_gherkin_error(msg)

您展示的 try / except 方法实际上是完全正确的,因为它展示了您在现实生活中实际使用代码的方式。但是,您不完全喜欢它是有原因的。它会导致如下问题:

Scenario: correct password accepted
Given that I have a correct password
When I attempt to log in  
Then I should get a prompt

Scenario: correct password accepted
Given that I have a correct password
When I attempt to log in 
Then I should get an exception

如果我在没有 try/except 的情况下编写步骤定义,那么第二种情况将失败。如果我用 try/except 编写它,那么第一种情况有隐藏异常的风险,特别是如果异常发生在提示已经打印之后。

恕我直言,这些场景应该写成

Scenario: correct password accepted
Given that I have a correct password
When I log in  
Then I should get a prompt

Scenario: correct password accepted
Given that I have a correct password
When I try to log in 
Then I should get an exception

“我登录”这一步不应该用try; “我尝试登录”巧妙地匹配尝试并泄露可能不会成功的事实。

然后是关于两者之间几乎但不完全相同的步骤之间的代码重用的问题。可能我们不希望有两个都登录的功能。除了简单地调用一个通用的其他函数之外,您还可以在步骤文件末尾附近执行类似的操作。

@when(u'{who} try to {what}')
def step_impl(context):
    try:
        context.execute_steps("when" + who + " " + what)
        context.exception=None
    except Exception as e:
        context.exception=e

这会自动将所有包含单词“try to”的步骤转换为具有相同名称但删除了 try 的步骤,然后用 try/except 保护它们。

关于何时实际应该在 BDD 中处理异常存在一些问题,因为它们对用户不可见。这不是这个问题的答案的一部分,所以我把它们放在 separate posting.

“玛丽·伯格曼”的回应应该是被接受的。我只缺少一件事:验证步骤引发的完整异常,而不仅仅是它的消息。

接受的“Garry”响应很好但对我来说并不完整,因为它不处理在没有验证异常的情况下使用“when”步骤的情况。通常 when 步骤是一个不会总是引发异常的操作,它取决于上下文。在仅使用“何时”步骤(因为它应该通过)的场景中,有一天,该步骤可能会因为回归而失败。如果它出现,异常会被静默捕获,异常不会显示在报告中并且场景会继续。这会导致误报。