【问题标题】:Handling Exceptions in Python Behave Testing framework在 Python 行为测试框架中处理异常
【发布时间】:2015-01-12 03:41:39
【问题描述】:

我一直在考虑从鼻子切换到行为测试(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)

我在行为中找不到类似的东西(不在源代码中,不在示例中,不在此处)。能够指定可能在场景大纲中引发的异常肯定会很棒。

有人走过这条路吗?

【问题讨论】:

  • 在我看来,确保代码在某些场景下抛出某些异常是一个非常标准的测试方法。也有利于向客户端代码显示它可以预期的行为。当我对此进行测试时,我不会测试失败!无论如何,这是大多数测试框架的标准功能。
  • 也许,您可以通过stackoverflow.com/questions/67338470/… 提供帮助。或者至少对于我关于util.show_gherkin_error(msg) 的问题 - 请参阅我对 Mary Bergman 的回答的评论。

标签: python bdd nose nosetests python-behave


【解决方案1】:

我自己对 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

【讨论】:

  • 结束了放弃行为。拥有单独的黄瓜规范文件和强制几乎所有内容都进行多步骤测试的约定实在是太多的工作。我发现它破坏了我的开发流程。回到鼻子!
【解决方案2】:

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

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

我的建议是

  • 在失败的语句之前有期望异常
  • 在 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)

我导入我修改后的给定/然后/时而不是行为,并添加到我的 environment.py 启动上下文。预期在场景之前失败并在之后检查它:

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

【讨论】:

  • 在 Python 3 和 Behave 1.2.6 的上下文中util.show_gherkin_error(msg) 是什么?
【解决方案3】:

您展示的 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/except 保护它们。

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

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

    • 嗨@jenisys;谢谢你的行为顺便说一句我认为这里的问题不仅仅是匹配器。当您运行“何时”步骤时,不清楚您应该匹配什么。例如如果您匹配异常但不对其进行测试,则可以稍后屏蔽不同的异常。如果您在“何时”步骤中遇到异常,那么它应该作为测试错误传播。
    【解决方案5】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-03-27
      • 2021-11-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-08
      • 2012-06-10
      相关资源
      最近更新 更多