下文將展示一個測試驅動開發(TDD)的例項,希望能給想要開始實踐TDD的朋友一個演示。本例項將採用python進行演示,如果您之前沒用過python,也不必擔心,這是一個很簡潔易懂的語言。本人也會在下文例項中對出現的python語法進行解釋。
假設python語法中沒有 乘法(*) 這個操作符,我們要自己實現一個簡單的乘法運算函式。
開始之前我們要記住TDD的核心,那就是:寫功能程式碼之前先寫測試,用測試去“驅動”功能實現。換句話說就是“只有在測試失敗的時候才能新增或修改功能程式碼”。具體步驟如下:
- 新增測試。
- 執行測試(測試失敗)。
- 實現功能程式碼。
- 執行測試並通過測試(若測試失敗,回到第3步)。
- 回到第一步。
步驟很簡單,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 != -50
, multiply(-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
成功了!!心思細膩的小朋友可能會發現如果傳參a或b為0的時候會怎麼樣?的確,這樣的邊界情況沒有考慮到,這也是程式設計讓大部分人覺得頭疼的地方。我們新增測試看看
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 = 0
,a = 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的決定。
擴充套件閱讀:
- 如果python是你的主要開發語言:Python測試驅動開發
- 或許更加通用的書籍:測試驅動開發