測試驅動開發(TDD)例項演示

紅提發表於2020-05-14

下文將展示一個測試驅動開發(TDD)的例項,希望能給想要開始實踐TDD的朋友一個演示。本例項將採用python進行演示,如果您之前沒用過python,也不必擔心,這是一個很簡潔易懂的語言。本人也會在下文例項中對出現的python語法進行解釋。

假設python語法中沒有 乘法(*) 這個操作符,我們要自己實現一個簡單的乘法運算函式。

開始之前我們要記住TDD的核心,那就是:寫功能程式碼之前先寫測試,用測試去“驅動”功能實現。換句話說就是“只有在測試失敗的時候才能新增或修改功能程式碼”。具體步驟如下:

  1. 新增測試。
  2. 執行測試(測試失敗)。
  3. 實現功能程式碼。
  4. 執行測試並通過測試(若測試失敗,回到第3步)。
  5. 回到第一步。

步驟很簡單,3~4步可以重複執行n遍直到測試通過。

希望我們可以一起實現這個demo,這樣你就能和我一起體驗到TDD的樂趣。

  • 注意:請確保先安裝python,unix系統預設會安裝python

執行python --version,如果列印出python版本則表示已經安裝了python。如下:

~ python --version
Python 3.7.2

假設我們要實現一個名為multiply的函式,函式可以輸入兩個數字引數,返回兩個數字相乘的結果。建立一個檔名為tdd-demo.py,我們可以先寫如下測試程式碼:

"""tdd-demo.py"""
import unittest


class MultiplyTest(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)


if __name__ == '__main__':
    unittest.main()
  • unittest是python用於單元測試的標準庫,它提供更人性化的測試結果展示,同時也提供很多測試方法和鉤子。本文只用到assertEqual(a, b),意思是斷言 a == b,想了解更多關於unittest框架的讀者可以點選這裡
  • 程式碼尾部的if __name__ == '__main__':unittest.main()代表執行該檔案時執行unittest.main(),既:執行單元測試。

上面測試程式碼的意思就是測試 multiply(2, 3) 應該等於 6

執行 python tdd-demo.py 命令。

➜  python tdd-demo.py
E
======================================================================
ERROR: test_multiply (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tdd-demo.py", line 7, in test_multiply
    self.assertEqual(multiply(2, 3), 6)
NameError: name 'multiply' is not defined

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

執行後返回如上報錯,NameError: name 'multiply' is not defined,意思是multiply這個函式未定義。知道測試失敗的原因後,我們新增功能程式碼如下:

"""tdd-demo.py"""
import unittest


def multiply():
    pass


class MultiplyTest(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)


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

我們定義了一個空白的函式multiply,現在再跑一次測試看看

➜  python tdd-demo.py
E
======================================================================
ERROR: test_multiply (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tdd-demo.py", line 11, in test_multiply
    self.assertEqual(multiply(2, 3), 6)
TypeError: multiply() takes 0 positional arguments but 2 were given

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

上述錯誤的意思是函式multiply定義了0個傳參,但是multiply(2, 3)傳遞了2個引數。由此我們知道是忘記給函式multiply定義傳參了,我們修改程式碼如下:

"""tdd-demo.py"""
import unittest


def multiply(a, b):
    pass


class MultiplyTest(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)


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

重新執行 python tdd-demo.py

➜  python tdd-demo.py
F
======================================================================
FAIL: test_multiply (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tdd-demo.py", line 11, in test_multiply
    self.assertEqual(multiply(2, 3), 6)
AssertionError: None != 6

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

和預期的一樣,AssertionError: None != 6,函式multiply(2, 3)的結果返回None,沒定義函式的返回值當然返回None(python中未定義函式返回值時,則預設返回None),讓我們來通過這個測試!

"""tdd-demo.py"""
import unittest


def multiply(a, b):
    return 6


class MultiplyTest(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)


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

上面用了一個取巧的辦法,不過現在先別急,我們會在後面修改它,我們先執行測試看看。

➜  python tdd-demo.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

成功了!!
但是我們知道我們的multiply函式現在根本還不能用!每次乘法都返回6的計算器也沒人敢用!!
這時候測試都通過了,想要修改功能程式碼,我們就需要新增新的測試了。
為了節省篇幅,下文只列出部分程式碼。

class MultiplyTest(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)
        self.assertEqual(multiply(3, 5), 15)

我們新增了新的測試,再執行看看。

➜  python tdd-demo.py
F
======================================================================
FAIL: test_multiply (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tdd-demo.py", line 12, in test_multiply
    self.assertEqual(multiply(3, 5), 15)
AssertionError: 6 != 15

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

很好,一切如我們所預料,取巧的辦法肯定沒法通過完善的測試,乖乖寫程式碼吧。
還記得上文說的,不能夠使用 乘法(*) 操作符嗎? 但是 加法(+) 是可以使用的,我們知道 2 * 3 = 3 + 3,也就是2個3相加,乘法a * b 其實是a個b相加。知道原理後我們可以實現程式碼如下:

def multiply(a, b):
    result = 0
    while a > 0:
        a = a - 1
        result = result + b
    return result

while a > 0: 表示當 a > 0 時,執行縮排塊裡的內容既:

        a = a - 1
        result = result + b

執行測試看看

python tdd-demo.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

成功了!我們用大點的數字相乘看看。

class MultiplyTest(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)
        self.assertEqual(multiply(3, 5), 15)

    def test_multiply_with_larger_number(self):
        self.assertEqual(multiply(512, 2), 1024)
        self.assertEqual(multiply(10000, 10000), 100000000)

具體的測試程式碼,可以自行發揮,但是數字不要太大,我們的計算器效能不太好^_^。我們執行測試看看

➜  python tdd-demo.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

成功了哈哈,那如果是傳入的引數是負數呢?現在讓我新增負數的測試程式碼看看(ps:記住,想要新增功能前先新增測試程式碼。能通過測試的程式碼就是沒問題的程式碼。)

class MultiplyTest(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)
        self.assertEqual(multiply(3, 5), 15)

    def test_multiply_with_larger_number(self):
        self.assertEqual(multiply(512, 2), 1024)
        self.assertEqual(multiply(10000, 10000), 100000000)

    def test_multiply_with_negative_number(self):
        self.assertEqual(multiply(-5, 10), -50)
        self.assertEqual(multiply(5, -10), -50)
        self.assertEqual(multiply(-5, -5), 25)

執行測試

➜  python tdd-demo.py
..F
======================================================================
FAIL: test_multiply_with_negative_number (__main__.MultiplyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tdd-demo.py", line 23, in test_multiply_with_negative_number
    self.assertEqual(multiply(-5, 10), -50)
AssertionError: 0 != -50

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

果然失敗了,AssertionError: 0 != -50multiply(-5, 10) 返回了0,讓我們看看程式碼哪裡出問題了。找到了 while a > 0: 因為a = -5 < 0 所以直接返回result = 0 了。知道原因後,我們可以把負數符號先抽出來,讓我們改一下程式碼。(ps:可以自己試著去實現,期間不斷地靠測試程式碼來驗證,你會發現有測試程式碼作保證,功能程式碼便可以大膽試錯,後面你會發現實現功能會比正常開發快很多。)最後我寫出如下程式碼:

def multiply(a, b):
    result = 0
    is_negative = False
    
    if a < 0:
        is_negative = True
        a = - a
        
    while a > 0:
        a = a - 1
        result = result + b
        
    if is_negative:
        result = - result
        
    return result

判斷 a 是否小於0,如果是的話,就標記一下並把負號抽出來,再把結果新增上負號,是不是很像初學負數運算時的計算步驟。我們再跑測試看看

➜  python tdd-demo.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

成功了!!心思細膩的小朋友可能會發現如果傳參ab0的時候會怎麼樣?的確,這樣的邊界情況沒有考慮到,這也是程式設計讓大部分人覺得頭疼的地方。我們新增測試看看

class MultiplyTest(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)
        self.assertEqual(multiply(3, 5), 15)

    def test_multiply_with_larger_number(self):
        self.assertEqual(multiply(512, 2), 1024)
        self.assertEqual(multiply(10000, 10000), 100000000)

    def test_multiply_with_negative_number(self):
        self.assertEqual(multiply(-5, 10), -50)
        self.assertEqual(multiply(5, -10), -50)
        self.assertEqual(multiply(-2, -3), 6)

    def test_multiply_with_zero_number(self):
        self.assertEqual(multiply(0, 5), 0)
        self.assertEqual(multiply(2, 0), 0)
        self.assertEqual(multiply(0, 0), 0)

執行測試

➜  python tdd-demo.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

居然成功了!自己都沒想到,我們回顧一下功能程式碼,最初定義了result = 0a = 0 的時候就直接返回了result,也就是0

到這裡,本文的TDD之旅就結束了。如果你不盡興,可以試著用TDD的方式來實現多個(大於2個)數字相乘,或者當輸入的引數不是數字的時候,返回或丟擲一些有用的message提示。這也是實際開發中經常用到的場景。

最終的程式碼如下:

"""tdd-demo.py"""
import unittest


def multiply(a, b):
    result = 0
    is_negative = False

    if a < 0:
        is_negative = True
        a = - a

    while a > 0:
        a = a - 1
        result = result + b

    if is_negative:
        result = - result

    return result


class MultiplyTest(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)
        self.assertEqual(multiply(3, 5), 15)

    def test_multiply_with_larger_number(self):
        self.assertEqual(multiply(512, 2), 1024)
        self.assertEqual(multiply(10000, 10000), 100000000)

    def test_multiply_with_negative_number(self):
        self.assertEqual(multiply(-5, 10), -50)
        self.assertEqual(multiply(5, -10), -50)
        self.assertEqual(multiply(-2, -3), 6)

    def test_multiply_with_zero_number(self):
        self.assertEqual(multiply(0, 5), 0)
        self.assertEqual(multiply(2, 0), 0)
        self.assertEqual(multiply(0, 0), 0)


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

ps:本文的目的就是簡單介紹一下TDD的例項和步驟,借用乘法multiply的這個函式來演示TDD在具體開發中的實踐。具體程式碼實現肯定有不完善的地方,如果有錯誤或遺漏的地方,望讀者指出。

如果您看完文章,對於TDD有了不一樣的認識,或想要在接下來的開發中使用TDD,那麼本文就已經完成了它的使命。

最後祝每個程式設計師都能“控制”程式碼而不是被程式碼“控制”,而“控制”程式碼最好的方式就是用測試程式碼“控制它”。本文只是以一個簡單的乘法函式作為TDD的演示,讀者可能在實際的專案開發中會遇到很多測試程式碼“不好寫”的情況。同時迫於專案deadline的壓力,想要快速的實現功能並上線,就打算放棄使用TDD的方式。但我也希望你能夠保持先寫測試的習慣,幾個月後,我相信你會感謝自己當時堅持TDD的決定。

擴充套件閱讀:

相關文章