[Twisted] Part 15: Tested Poetry

本文由Dave的Part 15: Tested Poetry翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。

Introduction

我們在探索Twisted時寫了很多程式碼,但目前為止我們忽略了寫一些很重要的東西-測試。而你可能很好奇你如何用同步框架像是Python附帶的unittest,來測試非同步的程式碼。簡單來說,你不能。像我們已經發現的,同步與非同步程式碼不能混用,至少不容易。

幸運的是,Twisted包含了自己的測試框架,叫做trial它支援測試非同步程式碼(而且你也可以用它測試同步程式碼)。

我們將假設你已經熟悉了unittest與類似測試框架的基本機制,這些框架中你透過定義一個繼承指定父類別(通常叫做TestCase之類的)的類別來建立測試,而類別中每個以「test」開頭的方法就會被認定是一項測試。這些框架負責找出所有的測試方法,配合上選擇性的setUp和tearDown,一起依序執行它們,最後報告結果。

The Example

你可以在tests/test_poetry.py找到一些測試的範例。為了確保所有我們的範例都是獨立運作(因此你不必擔心有關PYTHONPATH的設定),我們已經將所有所需的程式碼複製到測試模組中。當然,通常你只需要import你想要測試的模組就好。

這個範例透過由用戶端從測試伺服器取回詩歌,來測試詩歌用戶端與伺服器。為了提供用於測試的詩歌伺服器,我們在我們的測試案例中實現了setUp方法:
class PoetryTestCase(TestCase):

    def setUp(self):
        factory = PoetryServerFactory(TEST_POEM)
        from twisted.internet import reactor
        self.port = reactor.listenTCP(0, factory, interface="127.0.0.1")
        self.portnum = self.port.getHost().port

setUp方法以測試詩歌建立詩歌伺服器,並監聽一個隨機的開放埠口。我們會保存這個埠號,這樣實際測試時如果我們需要就可以使用它。當然,我們在測試完成時,在tearDown中清理掉測試伺服器:
    def tearDown(self):
        port, self.port = self.port, None
        return port.stopListening()

讓我們來看看我們第一個測試test_client,我們使用get_poetry從測試伺服器取回詩歌,並驗證這首詩歌是我們所要的:
    def test_client(self):
        """The correct poem is returned by get_poetry."""
        d = get_poetry('127.0.0.1', self.portnum)

        def got_poem(poem):
            self.assertEquals(poem, TEST_POEM)

        d.addCallback(got_poem)

        return d

注意我們的測試函式是回傳deferred實例。在trail框架中,每個測試方法都作為callback執行。這表示reactor正在執行,而我們能在測試過程中執行非同步的操作。我們只需要讓框架知道我們的測試是非同步的,所以我們用常見Twisted的方法讓框架知道-回傳deferred實例。

trial框架在呼叫tearDown方法之前,會一直等待直到deferred實例被觸發,並且如果deferred實例失敗那測試也會失敗(即最後一對callback/errback失敗)。如果我們的deferred觸發的時間太長,測試也會失敗,預設是兩分鐘。這也表示如果測試完成,我們就知道我們的deferred被觸發,因此我們的callback被觸發並執行assertEquals測試方法。

我們的第二個測試test_failure,驗證如果我們無法連接到伺服器,get_poetry會以適當的方式失敗:
    def test_failure(self):
        """The correct failure is returned by get_poetry when
        connecting to a port with no server."""
        d = get_poetry('127.0.0.1', 0)
        return self.assertFailure(d, ConnectionRefusedError)

這裡我們嘗試連接到無效埠口,然後使用trial提供的assertFailure方法。這個方法類似我們熟悉的assertRaises方法,但是是給非同步程式碼用的。如果給定的deferred因給定的例外而失敗,它回傳成功結果的deferred實例,否則回傳失敗結果的deferred實例。

你可以自行像這樣使用trail script執行這個測試:
trial tests/test_poetry.py

你應該會看到一些輸出顯示每個測試案例,以及一個OK告訴你每個測試都通過了。

Discussion

在基本的API方面,trial與unittest非常相似,所以開始去撰寫測試非常容易。如果你的測試使用非同步程式碼,只需要回傳deferred實例,而trial會處理其他的部分。你也可以從setUp或tearDown方法回傳deferred實例,如果它們也需要使用非同步。

任何來自測試的log訊息都會被收集在_trial_temp資料夾的檔案中,如果資料夾不存在trial會自動建立它。除了輸出到螢幕的錯誤之外,log對於debug失敗的測試是個有用的開始點。

圖33顯示了一個正在進行中的假想測試:
33.a%2Btrial%2Btest%2Bin%2Bprogress.png-Part 15: Tested Poetry

圖33.進行中的trial測試


如果你之前使用過類似的框架,這張圖應該是個熟悉的模型,除了所有測試相關的方法都回傳defered之外。

trial框架也很好的說明「進行非同步」是如何涉及串聯(cascade)整個程式的改變。為了測試(或任何函式或方法)成為非同步的,它必須:

  1. 沒有阻塞。
  2. 而且一般來說回傳deferred實例。

但這表示無論用什麼去呼叫,那個函式必須願意接收deferred實例,也不能阻塞(因此也可能回傳deferred實例)。如此一層一層的往上推論。因此就有了需要一個像trial的框架的需求,能處理這些回傳deferreds的非同步測試。

Summary

這就是我們對單元測試的看法。如果想看到多如何用Twisted程式碼撰寫單元測試的範例,你只需要看看Twisted本身的程式碼。Twisted框架附帶了一套非常大的單元測試,每個發布的版本又會加入新的測試。由於這些測試在確定能放入codebase之前的程式碼審查(code review)期間,都被Twisted專家仔細的審查過,因此它們提供了如何以正確的方式測試Twisted程式碼的優秀範例。

Part 16中,我們將使用Twisted工具將我們的詩歌伺服器轉化為一個真正的daemon。

Suggested Exercises

  1. 更改其中一個測試讓它失敗,然後再次執行trial來查看它的輸出。
  2. 閱讀線上的trial documentation
  3. 為我們在這個系列建立的其他詩歌服務撰寫測試。
  4. 探索Twisted中的some of the tests(譯註:原文中連結已經失效,所以我也不知道原作者指的是哪個網頁)。

留言