2020年2月1日 星期六

[Python] 我的pytest單元測試寫法

如題,是我個人常這樣寫,僅供參考。
(P.S. 此篇不算講解說明怎麼用mock patch,最好對@patch()這樣的寫法先有簡單的認知)

其實在這篇([Python] 測試中的mock技巧)我沒說明就這樣寫了,後來想說這也算是我摸索很久之後,自己覺得還不錯的寫法,就寫出來紀錄以及分享一下。

先來說說單元測試。

單元測試是針對程式中最小可測試單位進行測試,那這個最小可測試單位就是函式(function)。

要測試單一個函式,一定會遇到這個函式裡面呼叫了其他函式的狀況,假設我要測試A函式的功能,但在A函式裡它用到了B、C函式,假設B函式其實有bug,那我們在使用A函式時是非常有可能會出錯的,那這樣就造成我們針對A函式做的測試出錯真正的問題卻不是出在A函式,而是B函式,這會阻礙我們尋找root cause。

比較好的做法是若是在測試A函式,那在A函式中的其他函式最好是回傳給定的值,如此其他函式的錯誤不會影響到A函式的結果,而要做到這點就要仰賴mock patch技術。

接著,我們來看一下下面這個要被測試的程式碼吧。
def B(x):
    if x < 0:
        return False
    return True

def A(x):
    if B(x):
        return True
    return False

好,請各位發揮一下想像力,假設你是負責寫A函式的工程師,而B函式則是由別人寫的,結果好死不死負責寫B函式的人把「<」寫成了「>」。

想必你在試跑你的A函式時一定會發現跟你想得結果不一樣吧,因為這個A函式程式碼很短,所以你一看就覺得自己肯定沒錯,有問題的是B函式,但若是A函式很長,可能就要耗費你許多時間找原因,這就是上面說的「針對A函式做的測試出錯但真正的問題卻不是出在A函式」。

那再請各位試著想一下A、B函式的邏輯是什麼?

這是我的答案:
      A函式的程式邏輯:只要B回傳是True,就回傳True,若B回傳是False,則回傳False
      B函式的程式邏輯:x參數是負數的話,回傳False,其他則回傳True

不知你是否有接受A函式邏輯的描述?
因為我們在想邏輯的時候,多數會思考input參數與output結果的關係,因此就會覺得A函式的邏輯其實也是「x參數是負數的話,回傳False,其他則回傳True」,但這樣其實是連同B函式也一起看。
不妨先把B函式給遮住,再來想想怎麼描述A函式的邏輯,應該就懂意思了。

我們為A函式寫單元測試,目標就是要測試A函式的邏輯是否正確,


進入主題,可以看我們的pytest了,前面主要是在說單元測試裡做patch很重要。
from mock import patch
from func import A

class TestA():
    def setup(self):
        self.mock_b = patch("func.B").start()

    def teardown(self):
        patch.stopall()

    def test_if_B_is_true(self):
        self.mock_b.return_value = True
        assert A(1) is True

    def test_if_B_is_false(self):
        self.mock_b.return_value = False
        assert A(1) is False

若有寫過pytest應該能看懂我寫的程式碼
總之,我整理一下我喜歡的寫法的幾個要點:

1. 測試同一個函式,我會寫在一個class裡,像上面的test_if_B_is_true和test_if_B_is_false這兩個test case,我就都放在TestA裡

2. 接續第一點,這麼做是有用意的,那就是在每個test case前能跑一次setup(),而結束時會跑teardown()

3. setup(self)裡做的事是我會將被測試的函式中的其他函式都做patch,之後在test case裡設定回傳值,像上面的test_if_B_is_false,我就讓B函式固定都回傳False,所以即使我寫成A(1)結果還是False

4. teardown(self)則是要跑完一個test case能將patch停止重置,這是一定要寫的,別忘記


好吧,其實這樣的寫法也沒什麼特別的,沒有什麼比較厲害的效果,主要是省去需要在每個test case都掛相同@patch()的功夫吧



沒有留言:

張貼留言