Python之學會測試,讓開發更加高效(一)

  前幾天,聽了公司某位大佬關於編程心得的體會,其中講到了“測試驅動開發”,感覺自己的測試技能薄弱,因此,寫下這篇文章,希望對測試能有個入門。這段時間,筆者也體會到了測試的價值,一句話,學會測試,能夠讓你的開發更加高效。
  本文將介紹以下兩個方面的內容:

  • Test with Coverage
  • Mock

Test with Coverage

  測試覆蓋率通常被用來衡量測試的充分性和完整性。從廣義的角度講,主要分爲兩大類:面向項目的需求覆蓋率和更偏向技術的代碼覆蓋率。對於開發人員來說,我們更注重代碼覆蓋率。
  代碼覆蓋率指的是至少執行了一次的條目數佔整個條目數的百分比。如果條目數是語句,對應的就是代碼行覆蓋率;如果條目數是函數,對應的就是函數覆蓋率;如果條目數是路徑,對應的就是路徑覆蓋率,等等。統計代碼覆蓋率的根本目的是找出潛在的遺漏測試用例,並有針對性的進行補充,同時還可以識別出代碼中那些由於需求變更等原因造成的廢棄代碼。通常我們希望代碼覆蓋率越高越好,代碼覆蓋率越高越能說明你的測試用例設計是充分且完備的,但測試的成本會隨着代碼覆蓋率的提高而增加。
  在Python中,coverage模塊幫助我們實現了代碼行覆蓋率,我們可以方便地使用它來完整測試的代碼行覆蓋率。
  我們通過一個例子來介紹coverage模塊的使用。
  首先,我們有腳本func_add.py,實現了add函數,代碼如下:

# -*- 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)如下:

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文件,就能看到本次執行的覆蓋率情況,如下圖:
測試覆蓋率結果總覽
我們點擊func_add.py查看add函數測試的情況,如下圖:
func_add.py腳本的測試覆蓋率情況
可以看到,單元測試腳本test_func_add.py的前兩個測試用例只覆蓋到了add函數中左邊綠色的部分,而沒有測試到紅色的部分,代碼行覆蓋率爲75%。
  因此,還有兩種情況沒有覆蓋到,說明我們的單元測試中的測試用例還不夠充分。
  在test_func_add.py中,我們把main函數中的註釋去掉,把後兩個測試用例也添加進來,這時候我們再運行上面的coverage模塊的命令,重新生成htmlcov後,func_add.py的代碼行覆蓋率如下圖:
增加測試用例後,func_add.py腳本的測試覆蓋率情況
  可以看到,增加測試用例後,我們調用的add函數代碼行覆蓋率爲100%,所有的代碼都覆蓋到了。

Mock

  Mock這個詞在英語中有模擬的這個意思,因此我們可以猜測出這個庫的主要功能是模擬一些東西。準確的說,Mock是Python中一個用於支持單元測試的庫,它的主要功能是使用mock對象替代掉指定的Python對象,以達到模擬對象的行爲。在Python3中,mock是輔助單元測試的一個模塊。它允許您用模擬對象替換您的系統的部分,並對它們已使用的方式進行斷言。
  在實際生產中的項目是非常複雜的,對其進行單元測試的時候,會遇到以下問題:

  • 接口的依賴
  • 外部接口調用
  • 測試環境非常複雜

  單元測試應該只針對當前單元進行測試, 所有的內部或外部的依賴應該是穩定的, 已經在別處進行測試過的。使用mock 就可以對外部依賴組件實現進行模擬並且替換掉, 從而使得單元測試將焦點只放在當前的單元功能。
  我們通過一個簡單的例子來說明mock模塊的使用。
  首先,我們有腳本mock_multipy.py,主要實現的功能是Operator類中的multipy函數,在這裏我們可以假設該函數並沒有實現好,只是存在這樣一個函數,代碼如下:

# -*- coding: utf-8 -*-
# mock_multipy.py

class Operator():

    def multipy(self, a, b):
        pass

  儘管我們沒有實現multipy函數,但是我們還是想對這個函數的功能進行測試,這時候我們可以藉助mock模塊中的Mock類來實現。測試的腳本(mock_example.py)代碼如下:

# -*- 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()

讓我們對上述的代碼做一些說明。

op.multipy = mock.Mock(return_value=15)

通過Mock類來模擬調用Operator類中的multipy()函數,return_value 定義了multipy()方法的返回值。

result = op.multipy(4, 5)

result值調用multipy()函數,輸入參數爲4,5,但實際並未調用,最後通過assertEqual()方法斷言,返回的結果是否是預期的結果爲15。輸出的結果如下:

Ran 1 test in 0.002s

OK

  通過Mock類,我們即使在multipy函數並未實現的情況下,仍然能夠通過想象函數執行的結果來進行測試,這樣如果有後續的函數依賴multipy函數,也並不影響後續代碼的測試。
  利用Mock模塊中的patch函數,我們可以將上述測試的腳本代碼簡化如下:

# -*- 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腳本,代碼如下:

# -*- 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腳本如下:

# -*- 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

  本次分享到此結束,感謝大家的閱讀~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章