單元測試學習

發表於2017-05-28

單元測試是個好東西,但是如果程式碼中多處有資料庫訪問(讀/寫),或者程式碼中包含一些複雜的物件,真實環境中難以被觸發的物件的時候,該如何寫單元測試呢?

使用模擬物件機制測試python程式碼,模擬物件(mock object)可以取代真實物件的位置,用於測試一些與真實物件進行互動或依賴真實物件的功能,模擬物件的目的就是建立一個輕量級的,可控制的物件來代替測試中需要的真實物件,模擬真實物件的行為和功能,方便測試。

Stub和Mock以及Fake的理解

Stub: For replacing a method with code that returns a specified result

簡單來說就是可以用stub去fake(偽造)一個方法,阻斷原來方法的呼叫

Mock: A stub with an expectations that the method gets called

簡單來說mock就是stub + expectation, 說它是stub是因為它也可以像stub一樣偽造方法,阻斷對原來方法的呼叫, expectation是說它不僅偽造了這個方法,還期望你(必須)呼叫這個方法,如果沒有被呼叫到,這個test就fail了

Fake: objects actually have working implementations, but usually take some shortcut which makes them not suitable for production

簡單來說就是一個真實物件的一個輕量級的完整實現

mock物件的使用範疇: 真實物件具有不可確定的行為,產生不可預測的結果(如:天氣預報) 真實物件很難被建立 真實物件的某些行為很難被觸發

Fudge

Fudge是一個類似於Java中的JMock的純python的mock測試模組,主要功能就是可以偽造物件,替換程式碼中真實的物件,來完成測試。fudge主要用來模擬那些在應用中不容易構造或者比較複雜的物件(如專案中涉及mongodb或者redis模組,使用fudge後在測試的時候可以不需要真正的redis環境就能測試程式碼),從而使測試順利進行。

如何使用FUDGE

因為twitter_oauth是獨立的模組,因此只要呼叫了正確的方法,post_msg_to_twitter方法就一定能正確執行。Twitter在大陸沒法直接請求訪問,那怎麼測試知道它沒有問題呢? 使用fudge就能完成我們的任務,把twitter相關的物件偽造(fake)出來,只要我們自己的業務邏輯測試正確,那麼測試就通過。

  • patch裝飾器會在測試階段根據裝飾器裡面的引數偽造物件,作為測試方法test_post_to_twitter的引數。這些偽造的物件就是stub或者mock或者是fake
  • Fudge可以根據你的需求嚴謹或隨意的宣告expectation。
    • 如果你不關心具體的引數,就可以呼叫fudge.Fake.with_args()不需要指定任何引數,如果要指定的話就必須是指定正確的引數(換句話說就是不能隨意指定)
    • 如果你不關心方法呼叫與否,那麼就可以用fudge.Fake.provides()代替fudge.Fake.expects(),這樣即使程式碼中沒有呼叫,測試用例也不會fail
    • 如果不關心方法的引數的具體值,那麼可以用fudge.Fake.with_arg_count()來代替fudge.Fake.with_args()

fudge模組

FUDGE

  • fudge.patch(*obj_paths):測試裝飾器,裡面的引數都將作為fake物件將匯出作為測試方法的引數使用。

    patch方法會去呼叫fudge.clear_calls(),fudge.verify()和fudge.clear_expectations(),verify()方法才是真正驗證所有方法是不是按照期待的那些呼叫了。

    上面這個test函式如果沒有用fudge.patch(), fudge.test() 或者 fudge.with_fakes()修飾,那麼fudge就不會主動去驗證方法是否得到執行,必須加上fudge.verify()方法才會觸發呼叫。加上verify()就會提示你connect沒有被呼叫:
    • fudge.test:裝飾器,直接使用fake,而不是通過patch@fudge.test def test(): db = fudge.Fake(‘db’).expects(‘connect’) 不過絕大多數時候你都應該使用fudge.patch而不是fudge.test
  • fudge.Fake:這個一個類,用來替換真實物件的fake物件,如上例
  • fudge.calls(call):重新定義一個call,相當於給call換一個名字
  • expects(call_name):表示期待呼叫call_name方法
  • expects_call():表示該對像將得到呼叫
  • provides(call_name):這個方法與expects的區別是call_name可以沒有被呼叫

更多參考:fudge

FUDGE.INSPECTOR

fudge.inspector.ValueInspector例項可以作為一種更具表現力的物件(Value inspector)傳遞給fudge.Fake.with_args()方法,為了更方便記憶ValueInspector例項簡稱為arg

上面的測試程式碼就表示傳遞給save方法的引數必須是以.jpg結尾的值,否則測試沒法通過

  • arg.any():表示沒有任何約束
  • contains(part):必須包含指定的part引數
  • has_attr(**attributes):傳遞給方法的引數必須有屬性在指定的attributes中
    • passes_test(test):引數傳遞到test函式中必須返回True才能通過測試def is_valid(s): assert s in [‘apple’, ‘ms’, ‘fb’], (‘unexpected company %s’ % s) return Truedef test_passes_test(): system = fudge.Fake(‘system’).expects(‘set_company’).with_args(arg.passes_test(is_valid)) system.set_company(‘fb’)
  • startswith(part):引數必須以part開頭

更多參考:fudge.inspector

FUDGE.PATCHER

  • fudge.patcher.with_patched_object(obj, attr_name, patched_value):裝飾器,在被裝飾的方法呼叫前給attr_name一個新的值patched_value,方法執行完以後attr_name再恢復成原來的值

  • 輸出:

    這樣做的好處就是能獨立於每個測試而不影響原來物件的完整性。
  • fudge.pathcher.patched_context(obj, attr_name, patched_value):作用和上面的with_patched_object一樣,只是用法上不一樣而已,他的用法是:

    就是with語句的使用方式。

TORNADO.TEST

由於python的單元測試模組式同步的,測試tornado中的非同步程式碼有三種方式

  1. 使用類似tornado.gen的yield生成器 tornado.testing.gen_test.
    1. 2. 手工方式呼叫self.stop,self.waitclass MyTestCase2(AsyncTestCase): def test_http_fetch(self): client = AsyncHTTPClient(self.io_loop) client.fetch(“http://www.tornadoweb.org/”, self.stop) response = self.wait() # Test contents of response self.assertIn(“FriendFeed”, response.body)
    2. 3. 回撥函式的方式:class MyTestCase3(AsyncTestCase): def test_http_fetch(self): client = AsyncHTTPClient(self.io_loop) client.fetch(“http://www.tornadoweb.org/”, self.handle_fetch) self.wait() def handle_fetch(self, response): #此處產生的異常會通過stack+context傳播到self.wait方法中去 self.assertIn(“FriendFeed”, response.body) self.stop()

後兩者的原理是一樣的,wait方法會一直執行IOLoop,直到stop方法呼叫或者超時(timeout預設是5’s)2中的fetch的第二個引數self.stop相當於3中的self.handle_fetch,都是一個回撥函式,區別就在於把sotp當成回撥函式時,響應內容就會通過self.wait()函式返回,而像3中一樣寫一個自定義的回撥函式,響應內容就會作為引數傳遞給該函式。可以詳細檢視下tornado.testing.py這個檔案中的stop和wait方法。

預設情況下,每個單元會構造一個新的IOLoop例項,這個IOLoop是在構造HTTP clients/servers的時候使用。如果測試需要一個全域性的IOLoop,那麼就需要重寫get_new_ioloop方法。 原始碼:

tornado.testing.AsyncHTTPTestCase
這個類是AsyncTestCase的子類,一個測試用例會啟動一個HTTP server,AsyncHTTPTestCase的子類必須重寫get_app()方法,這個方法返回tornado.web.Application例項。application例項就是實際程式碼的application。

返回這個app就好了。測試用例通常使用self.http_client來請求(fetch)這個server上的url。

其實self.http_client就是一個AsyncHTTPClient例項,從原始碼中檢視到:

 

相關文章