Python之學會測試,讓開發更加高效(一)
阿新 • • 發佈:2020-04-27
前幾天,聽了公司某位大佬關於程式設計心得的體會,其中講到了“測試驅動開發”,感覺自己的測試技能薄弱,因此,寫下這篇文章,希望對測試能有個入門。這段時間,筆者也體會到了測試的價值,一句話,學會測試,能夠讓你的開發更加高效。
本文將介紹以下兩個方面的內容:
- Test with Coverage
- Mock
### Test with Coverage
`測試覆蓋率`通常被用來衡量測試的充分性和完整性。從廣義的角度講,主要分為兩大類:面向專案的`需求覆蓋率`和更偏向技術的`程式碼覆蓋率`。對於開發人員來說,我們更注重程式碼覆蓋率。
`程式碼覆蓋率`指的是至少執行了一次的條目數佔整個條目數的百分比。如果條目數是語句,對應的就是`程式碼行覆蓋率`;如果條目數是函式,對應的就是`函式覆蓋率`;如果條目數是路徑,對應的就是`路徑覆蓋率`,等等。統計程式碼覆蓋率的根本目的是找出潛在的遺漏測試用例,並有針對性的進行補充,同時還可以識別出程式碼中那些由於需求變更等原因造成的廢棄程式碼。通常我們希望程式碼覆蓋率越高越好,程式碼覆蓋率越高越能說明你的測試用例設計是充分且完備的,但測試的成本會隨著程式碼覆蓋率的提高而增加。
在Python中,`coverage`模組幫助我們實現了`程式碼行覆蓋率`,我們可以方便地使用它來完整測試的程式碼行覆蓋率。
我們通過一個例子來介紹`coverage`模組的使用。
首先,我們有指令碼`func_add.py`,實現了add函式,程式碼如下:
```python
# -*- coding: utf-8 -*-
def add(a, b):
if isinstance(a, str) and isinstance(b, str):
return a + '+' + b
elif isinstance(a, list) and isinstance(b, list):
return a + b
elif isinstance(a, (int, float)) and isinstance(b, (int, float)):
return a + b
else:
return None
```
在add函式中,分四種情況實現了加法,分別是字串,列表,屬性值,以及其它情況。
接著,我們用unittest模組來進行單元測試,程式碼指令碼(`test_func_add.py`)如下:
```python
import unittest
from func_add import add
class Test_Add(unittest.TestCase):
def setUp(self):
pass
def test_add_case1(self):
a = "Hello"
b = "World"
res = add(a, b)
print(res)
self.assertEqual(res, "Hello+World")
def test_add_case2(self):
a = 1
b = 2
res = add(a, b)
print(res)
self.assertEqual(res, 3)
def test_add_case3(self):
a = [1, 2]
b = [3]
res = add(a, b)
print(res)
self.assertEqual(res, [1, 2, 3])
def test_add_case4(self):
a = 2
b = "3"
res = add(a, b)
print(None)
self.assertEqual(res, None)
if __name__ == '__main__':
# 部分用例測試
# 構造一個容器用來存放我們的測試用例
suite = unittest.TestSuite()
# 新增類中的測試用例
suite.addTest(Test_Add('test_add_case1'))
suite.addTest(Test_Add('test_add_case2'))
# suite.addTest(Test_Add('test_add_case3'))
# suite.addTest(Test_Add('test_add_case4'))
run = unittest.TextTestRunner()
run.run(suite)
```
在這個測試中,我們只測試了前兩個用例,也就是對字串和數值型的加法進行測試。
在命令列中輸入`coverage run test_func_add.py`命令執行該測試指令碼,輸出結果如下:
```
Hello+World
.3
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
```
再輸入命令`coverage html`就能生成程式碼行覆蓋率的報告,會生成`htmlcov`資料夾,開啟其中的`index.html`檔案,就能看到本次執行的覆蓋率情況,如下圖:
![測試覆蓋率結果總覽](https://img-blog.csdnimg.cn/20200426210358432.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pjbGlhbjkx,size_16,color_FFFFFF,t_70)
我們點選`func_add.py`檢視add函式測試的情況,如下圖:
![func_add.py指令碼的測試覆蓋率情況](https://img-blog.csdnimg.cn/20200426210516759.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pjbGlhbjkx,size_16,color_FFFFFF,t_70)
可以看到,單元測試指令碼test_func_add.py的前兩個測試用例只覆蓋到了add函式中左邊綠色的部分,而沒有測試到紅色的部分,程式碼行覆蓋率為75%。
因此,還有兩種情況沒有覆蓋到,說明我們的單元測試中的測試用例還不夠充分。
在`test_func_add.py`中,我們把main函式中的註釋去掉,把後兩個測試用例也新增進來,這時候我們再執行上面的`coverage`模組的命令,重新生成`htmlcov`後,func_add.py的程式碼行覆蓋率如下圖:
![增加測試用例後,func_add.py指令碼的測試覆蓋率情況](https://img-blog.csdnimg.cn/20200426211207171.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pjbGlhbjkx,size_16,color_FFFFFF,t_70)
可以看到,增加測試用例後,我們呼叫的add函式程式碼行覆蓋率為100%,所有的程式碼都覆蓋到了。
### Mock
Mock這個詞在英語中有模擬的這個意思,因此我們可以猜測出這個庫的主要功能是模擬一些東西。準確的說,Mock是Python中一個用於支援單元測試的庫,它的主要功能是使用mock物件替代掉指定的Python物件,以達到模擬物件的行為。在Python3中,mock是輔助單元測試的一個模組。它允許您用模擬物件替換您的系統的部分,並對它們已使用的方式進行斷言。
在實際生產中的專案是非常複雜的,對其進行單元測試的時候,會遇到以下問題:
- 介面的依賴
- 外部介面呼叫
- 測試環境非常複雜
單元測試應該只針對當前單元進行測試, 所有的內部或外部的依賴應該是穩定的, 已經在別處進行測試過的。使用mock 就可以對外部依賴元件實現進行模擬並且替換掉, 從而使得單元測試將焦點只放在當前的單元功能。
我們通過一個簡單的例子來說明mock模組的使用。
首先,我們有指令碼`mock_multipy.py`,主要實現的功能是`Operator`類中的`multipy`函式,在這裡我們可以假設該函式並沒有實現好,只是存在這樣一個函式,程式碼如下:
```python
# -*- coding: utf-8 -*-
# mock_multipy.py
class Operator():
def multipy(self, a, b):
pass
```
儘管我們沒有實現`multipy`函式,但是我們還是想對這個函式的功能進行測試,這時候我們可以藉助mock模組中的Mock類來實現。測試的指令碼(`mock_example.py`)程式碼如下:
```python
# -*- coding: utf-8 -*-
from unittest import mock
import unittest
from mock_multipy import Operator
# test Operator class
class TestCount(unittest.TestCase):
def test_add(self):
op = Operator()
# 利用Mock類,我們假設返回的結果為15
op.multipy = mock.Mock(return_value=15)
# 呼叫multipy函式,輸入引數為4,5,實際並未呼叫
result = op.multipy(4, 5)
# 宣告返回結果是否為15
self.assertEqual(result, 15)
if __name__ == '__main__':
unittest.main()
```
讓我們對上述的程式碼做一些說明。
```python
op.multipy = mock.Mock(return_value=15)
```
通過Mock類來模擬呼叫Operator類中的multipy()函式,return_value 定義了multipy()方法的返回值。
```python
result = op.multipy(4, 5)
```
result值呼叫multipy()函式,輸入引數為4,5,但實際並未呼叫,最後通過assertEqual()方法斷言,返回的結果是否是預期的結果為15。輸出的結果如下:
```
Ran 1 test in 0.002s
OK
```
通過Mock類,我們即使在multipy函式並未實現的情況下,仍然能夠通過想象函式執行的結果來進行測試,這樣如果有後續的函式依賴multipy函式,也並不影響後續程式碼的測試。
利用Mock模組中的patch函式,我們可以將上述測試的指令碼程式碼簡化如下:
```python
# -*- coding: utf-8 -*-
import unittest
from unittest.mock import patch
from mock_multipy import Operator
# test Operator class
class TestCount(unittest.TestCase):
@patch("mock_multipy.Operator.multipy")
def test_case1(self, tmp):
tmp.return_value = 15
result = Operator().multipy(4, 5)
self.assertEqual(15, result)
if __name__ == '__main__':
unittest.main()
```
patch()裝飾器可以很容易地模擬類或物件在模組測試。在測試過程中,您指定的物件將被替換為一個模擬(或其他物件),並在測試結束時還原。
那如果我們後面又實現了multipy函式,是否仍然能夠測試呢?
修改`mock_multipy.py`指令碼,程式碼如下:
```python
# -*- coding: utf-8 -*-
# mock_multipy.py
class Operator():
def multipy(self, a, b):
return a * b
```
這時候,我們再執行`mock_example.py`指令碼,測試仍然通過,這是因為multipy函式返回的結果仍然是我們mock後返回的值,而並未呼叫真正的Operator類中的multipy函式。
我們修改`mock_example.py`指令碼如下:
```python
# -*- coding: utf-8 -*-
from unittest import mock
import unittest
from mock_multipy import Operator
# test Operator class
class TestCount(unittest.TestCase):
def test_add(self):
op = Operator()
# 利用Mock類,新增side_effect引數
op.multipy = mock.Mock(return_value=15, side_effect=op.multipy)
# 呼叫multipy函式,輸入引數為4,5,實際已呼叫
result = op.multipy(4, 5)
# 宣告返回結果是否為15
self.assertEqual(result, 15)
if __name__ == '__main__':
unittest.main()
```
`side_effect`引數和`return_value`引數是相反的。它給mock分配了可替換的結果,覆蓋了return_value。簡單的說,一個模擬工廠呼叫將返回side_effect值,而不是return_value。所以,設定`side_effect`引數為Operator類中的multipy函式,那麼return_value的作用失效。
執行修改後的測試指令碼,測試結果如下:
```
Ran 1 test in 0.004s
FAILED (failures=1)
15 != 20
Expected :20
Actual :15
```
可以發現,multipy函式返回的值為20,不等於我們期望的值15,這是side_effect函式的作用結果使然,返回的結果呼叫了Operator類中的multipy函式,所以返回值為20。
在`self.assertEqual(result, 15)`中將15改成20,執行測試結果如下:
```
Ran 1 test in 0.002s
OK
```
本次分享到此結束,感謝大家