Manejo de excepciones en el framework Python Behave Testing

He estado pensando en pasar de la nariz al comportamiento para las pruebas (mocha / chai, etc., me han echado a perder). Hasta ahora todo bien, pero no puedo encontrar otra forma de probar las excepciones, además de:

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

Con la nariz puedo anotar una prueba con

 @raises(KeyError) 

No puedo encontrar nada como esto en el comportamiento (no en la fuente, no en los ejemplos, no aquí). Seguro que sería grandioso poder especificar excepciones que podrían lanzarse en los contornos del escenario.

¿Alguien ha estado por este camino?

Yo mismo soy bastante nuevo en BDD, pero en general, la idea sería que las pruebas documenten qué comportamientos puede esperar el cliente, no las implementaciones de pasos. Así que espero que la forma canónica de probar esto sea algo como:

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

Con pasos definidos como:

 @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 

Si ese es un patrón común, puedes escribir tu propio decorador:

 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 

Este enfoque de prueba / captura de Barry funciona, pero veo algunos problemas:

  • Agregar un try / excepto a tus pasos significa que los errores estarán ocultos.
  • Agregar un decorador adicional no es elegante. Me gustaría que mi decorador fuera modificado en @where

Mi sugerencia es

  • tener la excepción esperada antes de la statement que falla
  • en el try / catch, boost si no se esperaba el error
  • en after_scenario, genera un error si no se encuentra el error esperado.
  • utilizar el modificado dado / cuando / luego en todas partes

Código:

  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) 

Importe mi archivo modificado dado / luego / cuando en lugar de comportarme, y lo agrego a mi environment.py iniciando context.expected falla antes del escenario y lo reviso después:

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

El enfoque try / except que muestra es en realidad completamente correcto porque muestra la forma en que realmente usaría el código en la vida real. Sin embargo, hay una razón por la que no te gusta por completo. Conduce a problemas feos con cosas como las siguientes:

 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 

Si escribo la definición del paso sin intentar / excepto entonces el segundo escenario fallará. Si lo escribo con try / except, entonces el primer escenario corre el riesgo de ocultar una excepción, especialmente si la excepción ocurre después de que el aviso ya se ha impreso.

En cambio, esos escenarios deberían, en mi humilde opinión, escribirse como algo así como

 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 

El paso “Me conecto” no debería usar try; El “Intento iniciar sesión” coincide perfectamente para intentarlo y revela el hecho de que podría no haber éxito.

Luego viene la pregunta acerca de la reutilización del código entre los dos pasos casi idénticos, pero no del todo. Probablemente no queremos tener dos funciones que inicien sesión. Además de tener simplemente una función común a la que llama, también podría hacer algo como esto cerca del final de su archivo de pasos.

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

Esto convertirá automáticamente todos los pasos que contengan la palabra “intentar” en pasos con el mismo nombre pero con intentar eliminarlos y luego protegerlos con un bash / excepto.

Hay algunas preguntas acerca de cuándo debería realmente lidiar con las excepciones en BDD ya que no son visibles para el usuario. Sin embargo, no es parte de la respuesta a esta pregunta, así que las he puesto en una publicación separada .

Comportarse no está en el negocio de la aserción del emparejador. Por lo tanto, no proporciona una solución para esto. Ya hay suficientes paquetes de Python que resuelven este problema.

VEA TAMBIÉN: behave.example: seleccione una biblioteca de coincidencia de aserción