Handling Exceptions in Python Behave Testing framework

This try/catch approach by Barry works, but I see some issues:

  • Adding a try/except to your steps means that errors will be hidden.
  • Adding an extra decorator is inelegant. I would like my decorator to be a modified @where

My suggestion is to

  • have the expect exception before the failing statement
  • in the try/catch, raise if the error was not expected
  • in the after_scenario, raise error if expected error not found.
  • use the modified given/when/then everywhere

Code:

    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)

I import my modified given/then/when instead of behave, and add to my environment.py initiating context.expected fail before scenario and checking it after:

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

I'm pretty new to BDD myself, but generally, the idea would be that the tests document what behaves the client can expect - not the step implementations. So I'd expect the canonical way to test this would be something like:

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

With steps defined like:

@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

If that's a common pattern, you could just write your own decorator:

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