Python和單元測試那些事兒

pythontab發表於2018-08-15

以前我是不寫任何測試的,後來偶爾寫單元測試,現在我主動寫單元測試 ----- 不得 不承認,測試是有其存在必要性的,要說為什麼的話,大概又會引發語言的強弱型別和 是否靜態語言之爭了吧。

就目前而言,個人認為寫單元測試的好處有以下幾點:

當修改了程式碼之後,單元測試可以保證API不會發生變化(假設原需求就不需API發生 變化)。這點可能一般情況下沒什麼感覺,但是當你去修改前輩留下的程式碼的時候, 你就會感謝他寫了單元測試,最少讓你知道了從功能上,這個函式是幹什麼的,而且 能保證你修改了函式內部實現,但是不影響函式功能。

寫單元測試的時候會回想函式的作用,從而自動對函式進行回想和 review。

缺點嘛:耗費時間。單元測試和文件一樣,屬於非常重要,但是非常耗費時間的工作, 因為要考慮齊全,考慮到的邊界條件越多,測試覆蓋率越高,程式越可靠,而想這些東 西是很耗費時間精力的。

吐槽完畢,我們來說說目前我知道的幾個和測試有關的東西(全程 Python 3)。

Mock

Mock是個好東西呀,遇到測試中出現的不可預知的或者不穩定因素,就用 Mock 來代 替。例如查詢資料庫(當然像目前我們用的MongoDB,由於特別靈活,可以直接在程式碼裡 把相應的collection替換掉),例如非同步任務等。舉個例子:

import logging
from unittest.mock import Mock
logging.basicConfig(level=logging.DEBUG)
#  code
class ASpecificException(Exception):
    pass
def foo():
    pass
def bar():
    try:
        logging.info("enter function <foo> now")
        foo()
    except ASpecificException:
        logging.exception("we caught a specific exception")
#  unittest
def test_foo():
    foo = Mock(side_effect=ASpecificException())  # noqa
    logging.info("enter function <bar> now")
    bar()
    logging.info("everything just be fine")
if __name__ == "__main__":
    test_foo()

執行一下:

root@arch tests: python test_demo.py
INFO:root:enter function <bar> now
INFO:root:enter function <foo> now
INFO:root:everything just be fine

一個簡單的測試就這麼寫好了。來,跟我念,Mock 大法好呀!


doctest

doctest屬於比較簡單的測試,寫在 docstring 裡,這樣既能測試用,又能當文件 示例,是在是好用之極啊。缺點是,如果測試太複雜,doctest就顯得太臃腫了(例如 如果測試之前要匯入一堆東西)。舉個例子:

import logging
logging.basicConfig(level=logging.DEBUG)
def foo():
    """A utility function that returns True
    >>> foo()
    True
    """
    return True
if __name__ == "__main__":
    import doctest
    logging.debug("start of test...")
    doctest.testmod()
    logging.debug("end of test...")

測試結果:

root@arch tests: python test_demo.py
DEBUG:root:start of test...
DEBUG:root:end of test...

unittest

這個文件確實有點長,我感覺還是仔細去讀一下文件比較好(雖然我也沒讀完)。

import unittest
class TestStringMethods(unittest.TestCase):
    def setUp(self):
        self.alist = []
    def tearDown(self):
        print(self.alist)
    def test_list(self):
        for i in range(5):
            self.alist.append(i)
if __name__ == '__main__':
    unittest.main()
root@arch tests: python test_demo.py
[0, 1, 2, 3, 4]
.
----------------------------------------------------------------------
Ran 1 test in 0.001s


OK

unittest框架配合上Mock,單元測試基本無憂啦。


pytest

上面的單元測試跑起來比較麻煩,當然也可以寫一個指令碼遍歷所有的單元測試檔案,然 後執行。不過 pytest 對unittest有比較好的支援。


pytest預設支援的是 函式 風格的測試,但是我們可以不用這一塊嘛(而且很多時候 還是很有用的)。走進專案根目錄,輸入 pytest 就可以啦。它會自動發現 test_ 開頭的檔案,然後執行其中 test_ 開頭的函式和 unittest 的 test_ 開頭的 方法。

root@arch tests: pytest
========================================================= test session starts =========================================================
platform linux -- Python 3.5.2, pytest-3.0.5, py-1.4.31, pluggy-0.4.0
rootdir: /root/tests, inifile:
collected 1 items
test_afunc.py .
====================================================== 1 passed in 0.03 seconds =======================================================
root@arch tests:

總結

編譯器沒給python做檢查,就只有靠我們手寫測試了 :(


另外其實 pytest 和 unittest 都有很多強大的特性,例如 fixture(不知道 咋翻譯好),例如 skip 掉某一部分測試。當然我也是知之甚少,所以還是看文件吧。


相關文章