Python單元測試——深刻理解unittest

單元測試的重要性就很少說了,可惡的是python中有太多的單元測試框架和工具,什麼unittest, testtools, subunit, coverage, testrepository, nose, mox, mock, fixtures, discover,再加上setuptools, distutils等等這些,先不說如何寫單元測試,光是怎麼運行單元測試就有N多種方法,再由於它是測試而非功能,是不少人沒興趣觸及的東西。可是做爲一個優秀的程序員,不只要寫好功能代碼,寫好測試代碼同樣的彰顯你的實力。如此多的框架和工具,很容易讓人困惑,困惑的緣由是由於並無理解它的基本原理,若是一些基本的概念都不清楚,怎麼可以寫出思路清晰的測試代碼? html

今天的主題就是unittest,做爲標準python中的一個模塊,是其它框架和工具的基礎,參考資料是它的官方文檔:http://docs.python.org/2.7/library/unittest.html和源代碼,文檔已經寫的很是好了,我在這裏記錄的主要是它的一些重要概念、關鍵點以及可能會碰到的一些坑,目的在於對unittest加深理解,而不是停留在泛泛的表面層上。 java

unittest是一個python版本的junit,junit是java中的單元測試框架,對java的單元測試,有一句話很貼切:Keep the bar green,相信使用eclipse寫過java單元測試的都心照不宣。unittest實現了不少junit中的概念,好比咱們很是熟悉的test case, test suite等,總之,原理都是相通的,只是用不一樣的語言表達出來。 python

在文檔的開篇就介紹了unittest中的4個重要的概念:test fixture, test case, test suite, test runner,我以爲只有理解了這幾個概念,才能真正的理解單元測試的基本原理,下面就主要圍繞這幾個概念來展開這篇文章。 程序員

首先經過查看unittest的源碼,來看一下這幾個概念,以及他們之間的關係,他們是如何在一塊兒工做的,其靜態類圖以下: 數據庫

unittest

  • 一個TestCase的實例就是一個測試用例。什麼是測試用例呢?就是一個完整的測試流程,包括測試前準備環境的搭建(setUp),執行測試代碼(run),以及測試後環境的還原(tearDown)。元測試(unit test)的本質也就在這裏,一個測試用例是一個完整的測試單元,經過運行這個測試單元,能夠對某一個問題進行驗證。
  • 而多個測試用例集合在一塊兒,就是TestSuite,並且TestSuite也能夠嵌套TestSuite。
  • TestLoader是用來加載TestCase到TestSuite中的,其中有幾個loadTestsFrom__()方法,就是從各個地方尋找TestCase,建立它們的實例,而後add到TestSuite中,再返回一個TestSuite實例。
  • TextTestRunner是來執行測試用例的,其中的run(test)會執行TestSuite/TestCase中的run(result)方法。
  • 測試的結果會保存到TextTestResult實例中,包括運行了多少測試用例,成功了多少,失敗了多少等信息。

這樣整個流程就清楚了,首先是要寫好TestCase,而後由TestLoader加載TestCase到TestSuite,而後由TextTestRunner來運行TestSuite,運行的結果保存在TextTestResult中,整個過程集成在unittest.main模塊中。 app

如今已經涉及到了test case, test suite, test runner這三個概念了,還有test fixture沒有提到,那什麼是test fixture呢??在TestCase的docstring中有這樣一段話: 框架

Test authors should subclass TestCase for their own tests. Construction and deconstruction of the test's environment ('fixture') can be implemented by overriding the 'setUp' and 'tearDown' methods respectively. dom

可見,對一個測試用例環境的搭建和銷燬,是一個fixture,經過覆蓋TestCase的setUp()和tearDown()方法來實現。這個有什麼用呢?好比說在這個測試用例中須要訪問數據庫,那麼能夠在setUp()中創建數據庫鏈接以及進行一些初始化,在tearDown()中清除在數據庫中產生的數據,而後關閉鏈接。注意tearDown的過程很重要,要爲之後的TestCase留下一個乾淨的環境。關於fixture,還有一個專門的庫函數叫作fixtures,功能更增強大,之後會介紹到。 eclipse

至此,概念和流程基本清楚了,下面經過簡單的例子再來實踐一下,就拿unittest文檔上的例子吧: wordpress

import random
import unittest

class TestSequenceFunctions(unittest.TestCase):

    def setUp(self):
        self.seq = range(10)

    def test_shuffle(self):
        # make sure the shuffled sequence does not lose any elements
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, range(10))

        # should raise an exception for an immutable sequence
        self.assertRaises(TypeError, random.shuffle, (1,2,3))

    def test_choice(self):
        element = random.choice(self.seq)
        self.assertTrue(element in self.seq)

    def test_sample(self):
        with self.assertRaises(ValueError):
            random.sample(self.seq, 20)
        for element in random.sample(self.seq, 5):
            self.assertTrue(element in self.seq)

if __name__ == '__main__':
    unittest.main()

TestSequenceFunctions繼承自unittest.TestCase,重寫了setUp()方法,而且定義了三個以'test'開頭的方法,那這個TestSequenceFunctions類究竟是個什麼呢?它是一個測試用例,仍是三個測試用例?說是三個測試用例的話,它自己繼承自TestCase,說是一個測試用例的話,裏面又有三個test_*()方法,明顯是三個測試用例。其實,咱們只要看一些TestLoader是如何加載測試用例的,就一清二楚了,在loader.TestLoader類中有一個loadTestsFromTestCase()方法:

def loadTestsFromTestCase(self, testCaseClass):
        """Return a suite of all tests cases contained in testCaseClass"""
        if issubclass(testCaseClass, suite.TestSuite):
            raise TypeError("Test cases should not be derived from TestSuite." \
                                " Maybe you meant to derive from TestCase?")
        testCaseNames = self.getTestCaseNames(testCaseClass)
        if not testCaseNames and hasattr(testCaseClass, 'runTest'):
            testCaseNames = ['runTest']
        loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
        return loaded_suite

getTestCaseNames()是從TestCase這個類中找全部以「test」開頭的方法,而後注意第9行,在構造TestSuite對象時,其參數使用了一個map方法,即對testCaseNames中的每個元素,使用testCaseClass爲其構造對象,其結果是一個TestCase的對象集合,能夠用下面的代碼來分步說明:

testcases = []
for name in testCaeNames:
    testcases.append(TestCase(name))
loaded_suite = self.suiteClass(tuple(testcases))

可見,對每個以test開頭的方法,都爲其構建了一個TestCase對象,值得注意的是,若是沒有定義test開頭的方法,而是將測試代碼寫到了一個名爲runTest的方法中,那麼會爲該runTest方法構建TestCase對象,若是定義了test開頭的方法,就會忽略runTest方法。

至此,基本就清楚了,每個以test開頭的方法,都會爲其構建TestCase對象,也就是說TestSequenceFunctions類中其實定義了三個TestCase,之因此寫成這樣,是爲了方便,由於這幾個測試用例的fixture是相同的,若是每個測試用例單獨寫成一個TestCase的話,會有不少的冗餘代碼。

明白了這些,文檔就能夠很輕鬆的看懂了,至於怎麼運行測試用例,以及其餘的內容,直接看文檔吧。

相關文章
相關標籤/搜索