2015-01-12 77 views
6

我一直在考慮從鼻子切換到行爲測試(摩卡/柴等寵壞了我)。到目前爲止好,但我似乎無法找出任何測試方式爲,除了例外:處理Python異常行爲測試框架

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

我找不到任何一個測試像這樣行爲(不是來源,不是在這些例子中,不在這裏)。如果能夠指定可能在方案大綱中引發的異常,那肯定會是盛大的。

任何人都在這條路上?

+0

在我看來,確保代碼在某些情況下拋出某些異常是一個非常標準的事情來測試。對於向客戶端代碼展示它所期望的行爲也很好。當我測試的時候,我沒有測試失敗!無論如何,這是大多數測試框架的一個非常標準的功能。 –

回答

6

我對BDD本人來說很新,但一般來說,這個想法是測試文檔記錄了客戶端可以預期的行爲 - 而不是步驟實現。所以我期望的規範的方法來測試,這將是這樣的:

@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 

如果這是一個共同的模式,你可以只寫你自己的裝飾:

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

隨着步驟等被定義

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 
+0

工作過的感謝!我的大腦昨晚都生鏽了。 –

+0

傷心放棄行爲。擁有單獨的黃瓜spec文件和強制幾乎所有東西都是多步驟測試的慣例只是太多的工作。我發現它破壞了我的開發流程。回到鼻子! –

0

行爲不在斷言匹配器業務。因此,它沒有爲此提供解決方案。已經有足夠的Python包來解決這個問題。

也可參見:巴里behave.example: Select an assertion matcher library

+0

Hi @jenisys;謝謝你的表現BTW 我認爲這裏的問題不僅僅是匹配者。當你運行你的「什麼時候」步驟時,你不應該清楚你應該匹配什麼。例如如果你在一個例外上匹配,但不測試它,稍後可能會掩蓋一個不同的例外。如果你在「when」步驟中遇到異常,那麼它應該作爲測試錯誤傳播。 – Michael

2

這try/catch語句的方式工作,但我看到了一些問題:

  • 添加一個try /除了你的步驟意味着錯誤將被隱藏。
  • 添加一個額外的裝飾是不雅觀的。我想我的裝飾是改性@where

我的建議是

  • 已在預期異常失敗的語句中的try/catch
  • 之前,募集如果是沒有預料到的錯誤
  • 在after_scenario中,如果預期錯誤未找到,則引發錯誤。
  • 使用修改後的定/時/然後到處

代碼:

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

在try /除了你展示實際上是完全正確的,因爲它表明你實際上在現實生活中使用的代碼的方式方法。但是,有一個原因,你不完全喜歡它。如果我寫,而不嘗試/除,則第二個方案將失敗步驟定義

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編寫它,那麼第一種情況有隱藏異常的風險,特別是在提示已經打印之後發生異常的情況下。

相反這些情況應,恕我直言,被寫成像

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 

的「我登錄」一步不應該使用嘗試; 「我嘗試登錄」匹配整齊地嘗試和放棄可能沒有成功的事實。

然後出現了兩個幾乎但不完全相同的步驟之間的代碼重用問題。可能我們不想有兩個登錄的功能。除了簡單地使用通用的其他功能外,您還可以在步驟文件結尾附近執行類似操作。

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

這將自動轉換包含這個詞的所有步驟「嘗試」到步驟名稱相同但嘗試刪除,然後/保護他們一試時除外。

關於在BDD中何時應該處理異常的問題有些問題,因爲它們不是用戶可見的。這不是這個問題的答案的一部分,但我已經把它們放在separate posting