Search code examples
pythonbddnosenosetestspython-behave

Handling Exceptions in Python Behave Testing framework


I've been thinking about switching from nose to behave for testing (mocha/chai etc have spoiled me). So far so good, but I can't seem to figure out any way of testing for exceptions besides:

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

With nose I can annotate a test with

@raises(KeyError)

I can't find anything like this in behave (not in the source, not in the examples, not here). It sure would be grand to be able to specify exceptions that might be thrown in the scenario outlines.

Anyone been down this path?


Solution

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