10

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

@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)

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

有人走过这条路吗?

4

5 回答 5

11

我自己对 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
于 2015-01-12T04:22:36.713 回答
6

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)

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

    def after_scenario(context, scenario):
        if context.expected_fail:
            msg = "Expected failure not found: %s" % (context.expected_fail.message)
            util.show_gherkin_error(msg)
于 2016-12-07T09:46:50.517 回答
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 中处理异常存在一些问题,因为它们对用户不可见。不过,这不是这个问题的答案的一部分,所以我将它们放在单独的帖子中。

于 2016-12-23T09:16:24.473 回答
1

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

还请参见: behavior.example:选择一个断言匹配器库

于 2015-02-20T22:04:22.677 回答
1

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

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

于 2020-08-25T00:26:20.737 回答