一文搞懂Python Unittest測試方法執行順序

米洛丶發表於2021-09-16

大家好~我是米洛

歡迎關注我的公眾號測試開發坑貨,一起交流!點贊收藏關注,不迷路。

Unittest

unittest大家應該都不陌生。它作為一款博主在5-6年前最常用的單元測試框架,現在正被pytest,nose慢慢蠶食

漸漸地,看到大家更多的討論的內容從unittest+HTMLTestRunner變為pytest+allure2等後起之秀

不禁感慨,終究是自己落伍了,跟不上時代的大潮了。

回到主題

感慨完了,回到正文。雖然unittest正在慢慢被放棄,但是它仍然是一款很全面的測試框架

今天在群裡看到有個群友的一番言論,激起了我的一番回憶。

自己以前是知道unittest的執行順序並不是按照編寫test方法的順序執行,而是按照字典序執行的。但遺憾的是我都是投機取巧去解決的問題(後面會講)。

下面我們就來探討下unittest類的test方法的執行順序問題。

原始碼初窺

研究一下原始碼(unittest.TestLoader)可以發現,在載入一個class下面的test方法的時候,原生Loader進行了排序,並且根據functools.cmp_to_key方法對測試方法列表進行了排序。

我們知道,unittest是不需要我們指定對應的方法,說白了,它是從類裡面自動獲取到我們們的方法,並約定了以test開頭的方法都會被視為測試方法。

可以看到testMethodPrefix,即測試方法字首,如果不是test開頭則直接return False

查詢一下self.sortTestMethodsUsing(這個是一個排序的方式)。

找到對應的排序方法

可以看到這個比較方法寫的很明確了,如果x < y那麼返回-1,x = y則返回0,x > y返回1。

其實大家可能不知道Python裡面的字串也是可以比較的,在此必須說明一下字典序。我們來看看這個例子:

a = "abc"
b = "abcd"
c = "abce"
print(a > b)
print(b > c)

猜猜看執行結果,很顯然,字典序的比較,是按A-Z的順序來比較的,如果字首一樣但長度不一樣,那麼長度長的那個,字典序靠後。

所以這裡面a < b < c

瞭解了字典序以後,我們就不難知道,在unittest裡面它尋找case的過程可以這樣簡化:

  • 找到對應類下面以test開頭的測試方法

  • 對他們進行字典序排序

  • 依次執行

    這樣就不難解釋為什麼我們有時候寫的case不按照自己想的順序來

回到問題的本質

搞清楚為什麼用例會亂,那就想到對應的解決方案。由於修改原始碼是不太合適的,那我們有2個策略去達成目的。

比如我有多個test方法:

class Testcase(unittest.TestCase):

    def setUp(self) -> None:
        pass

    def test_1(self):
        print("執行第一個")

    def test_2(self):
        print("第二個")

    def test_3(self):
        print("第三個")

    def test_10(self):
        print("第四個")

    def test_11(self):
        print("第五個")

    def tearDown(self) -> None:
        pass
        
if __name__ == "__main__":
    unittest.main()

執行起來,按照字典序,其實是1 10 11 2 3的順序。

可以看到現在還是不對的

1. 以字典序的方式編寫test方法

我們可以手動修改test方法的名稱,這也是我早前的處理方式。也就是說把想要先執行的case字典序排到前面:

class Testcase(unittest.TestCase):

    def setUp(self) -> None:
        pass

    def test_0_1(self):
        print("執行第一個")

    def test_0_2(self):
        print("第二個")

    def test_0_3(self):
        print("第三個")

    def test_1_0(self):
        print("第四個")

    def test_1_1(self):
        print("第五個")

    def tearDown(self) -> None:
        pass

我們可以把數字按位數拆開,個位數就把10位補0,這樣就能達到效果,如果會寫100個case,我們就需要補2個0,比如0_0_1,當然一個檔案裡面也不會有太多case。

如果遇到test_login這種怎麼辦呢,不是數字結尾的方法。

其實是一樣的,可以寫成test_數字_業務的模式。番貨寫了一個裝飾器專門解決這樣的問題,大家可以去參考下。

2. 迴歸本質,從根本解決問題

方案1用了番貨的裝飾器,好是好,但是改變了方法本身的名稱,我們其實可以針對他的排序方式入手,按照我們編寫case的順序排序測試方法,就能達到想要的目的。

說說思路:

  1. 手寫一個loader繼承自TestLoader類,改寫裡面的排序方法
  2. 在unittest執行的時候傳入這個新的loader

來看看完整程式碼,註釋裡面寫的很完善了。

import unittest


class MyTestLoader(unittest.TestLoader):
    def getTestCaseNames(self, testcase_class):
        # 呼叫父類的獲取“測試方法”函式
        test_names = super().getTestCaseNames(testcase_class)
        # 拿到測試方法list
        testcase_methods = list(testcase_class.__dict__.keys())
        # 根據list的索引對testcase_methods進行排序
        test_names.sort(key=testcase_methods.index)
        # 返回測試方法名稱
        return test_names


class Testcase(unittest.TestCase):

    def setUp(self) -> None:
        pass

    def test_1(self):
        print("執行第一個")

    def test_2(self):
        print("第二個")

    def test_3(self):
        print("第三個")

    def test_10(self):
        print("第四個")

    def test_11(self):
        print("第五個")

    def tearDown(self) -> None:
        pass


if __name__ == "__main__":
    unittest.main(testLoader=MyTestLoader())

執行一下還是不對

執行了一下還是不對,是不是哪裡出了什麼問題呢?

是因為pycharm有一種預設的unittest的除錯方法,我們要改成普通的方法去執行。

這種就是unittest的專屬測試模式

改成data(我的py檔名稱),然後點選debug按鈕

別選Python tests,選正常的Python

搞定

試試用控制檯執行:

也沒什麼毛病

今天的內容就講到這裡了,看懂的記得給個贊哦~

相關文章