1. 程式人生 > 其它 >Python個人學習筆記(10)——測試程式碼

Python個人學習筆記(10)——測試程式碼

技術標籤:Python個人學習筆記單元測試python程式語言

測試程式碼

編寫函式和類時,還可以為其編寫測試.通過測試,可確定程式碼面對各種輸出都能夠按要求的那樣工作.在程式中新增新的程式碼時,你也可以對其進行測試,確定它們不會破壞程式既有的行為.程式設計師都會犯錯,因此每個程式設計師都必須經常測試其程式碼,在使用者發現問題前找出它們.
學習目標:
學習如何使用Python模組unittest中的工具來測試程式碼.
學習編寫測試用例,核實一系列輸入都將得到預期的輸出

測試函式

要測試函式,得有要測試的程式碼.然後,大家也都會執行程式,然後進行相應的操作,再根據程式做出的應答判斷程式是否正常.但不可置否,這個步驟太繁瑣了.所幸python提供了一種自動測試函式輸出的高效方式.

  • 單元測試和測試用例
    python標準庫中的模組unittest提供了程式碼測試工具.單元測試用於核實函式的某個方面沒有問題;測試用例是一組單元測試,這些單元測試一起核實函式在各種行為都符合要求.良好的測試用例考慮到函式可能收到的各種輸入,包含針對所有這些情形的測試.全覆蓋式測試用例包含一整套單元測試,涵蓋了各種可能的函式使用方式.對於大型專案,要實現全覆蓋可能很難.通常,最初只需要針對程式碼的重要行為編寫測試即可,等專案被廣泛使用時在考慮全覆蓋.
  • 可通過的測試
    要為函式寫測試用例,可先匯入模組unittest以及要測試的函式,再建立一個繼承unittest.TestCase的類,並編寫一系列方法去對函式行為的不同方向進行測試.下面是一個簡單的函式:
    name_function.py
def get_formatted_name(first,last):
    """生成整潔的姓名"""
    full_name = first + ' ' + last
    return full_name.title()

下面是僅包含一個方法的測試用例,用於檢查get_formtted_name()在給定名和姓時,能否正常地工作:
test_name_function.py

import unittest

from name_function import get_formatted_name

class
NamesTestCase(unittest.TestCase): """測試name_function.py""" def test_first_last_name(self): """能夠正確地處理像Janis Joplin這樣的姓名嗎?""" formatted_name = get_formatted_name('janis','joplin') self.assertEqual(formatted_name,'Janis Joplin') unittest.main()

執行結果:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
  • 首先,我們匯入了模組unittest和要測試的函式get_formatted_name().
  • 建立一個NamesTestCase的類,用於包含一系列針對get_formatted_name()的單元測試.可以隨便對這個類命名,但最好讓它看起來要和要測試的函式相關,幷包含Test字樣.這個類必須繼承unittest.TestCase類——這樣python才知道如何執行你編寫的測試。
  • 在上面,NamesTestCase類只包含一個方法,用於測試get_formatted_name()的一個方面.
  • 在下面這條程式碼,我們使用了unittest類最有用的功能之一:一個斷言方法.斷言方法是用來核實得到的結果是否與期望的結果一致.這條程式碼行的意思是說:將formatted_name值與字串’Janis Joplin’作比較,如果它們相等,則萬事大吉,如果不相等,跟我說一聲.事實上,執行結果告訴我們,程式碼的功能跟預期一致.
self.assertEqual(formatted_name,'Janis Joplin')
  • 不能通過的測試
    我們修改get_formatted_name()方法:
    name_function.py
def get_formatted_name(first,middle,last):
    """生成整潔的姓名"""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()

再執行test_name_function.py程式,有:

E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
能夠正確地處理像Janis Joplin這樣的姓名嗎?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "d:\vscode\test.py", line 10, in test_first_last_name
    formatted_name = get_formatted_name('janis','joplin')
TypeError: get_formatted_name() missing 1 required positional argument: 'last'

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)

讓我們來分析一下:

  • 第一行輸出只有一個E,它指出測試用例中有一個單元測試導致了錯誤.
  • 接下來,我們看到NamesTestCase中的test_first_last_name導致了錯誤
  • 我們還看到一個標準的traceback,它指出函式呼叫get_formatted_name(‘janis’,‘joplin’)的問題.因為它缺少了一個必不可少的位置實參
  • 我們還看到了運行了一個單元測試
  • 最後還看到了一條資訊,它指出整個單元測試沒能通過,因為執行該測試用例時發生了一個錯誤.這條錯誤位於輸出末端,讓你一眼就能看到,你不必為獲悉有多少測試沒通過而翻閱長長的輸出.
  • 測試沒通過怎麼辦?
    我們對方法get_formatted_name()進行一些修改:
    • 讓中間名變為可選的
    • 在函式定義時,將形參middle移到形參末尾,並將其預設值設為一個空字串
    • 還要新增一個if測試,以便根據是否提供了中間名相應地建立姓名
      name_function.py
def get_formatted_name(first,last,middle=''):
    """生成整潔的姓名"""
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()

以及在類NamesTestCase中增加一個新的測試單元

import unittest

from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """測試name_function.py"""

    def test_first_last_name(self):
        """能夠正確地處理像Janis Joplin這樣的姓名嗎?"""
        formatted_name = get_formatted_name('janis','joplin')
        self.assertEqual(formatted_name,'Janis Joplin')

    def test_first_middle_last_name(self):
        """能正常處理像Wolfgang Amadeus Mozart這樣的姓名嗎?"""
        formatted_name = get_formatted_name('Wolfgang','Mozart','Amadeus')
        self.assertEqual(formatted_name,'Wolfgang Amadeus Mozart')

unittest.main()

執行結果:

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

現在,兩個測試用例都通過了.這讓我們深信這個函式既能正確地處理像Janis Joplin這樣的姓名,也能正確到處理像Wolfgang Amadeus Mozart這樣的姓名.

測試類

在前面,我們編寫了針對單個函式的的測試,下面來編寫針對類的測試

  • 各種斷言方法
    python在unittest.TestCase類中提供了很多斷言方法.前面說過,斷言方法檢查你認為應該滿足的條件是否得到滿足.如果該條件確實滿足,你對程式行為的假設就得到了確認,你就可以確信其中沒有錯誤.如果你認為應該滿足的條件沒有得到滿足,Python將引發異常.

下面列表中描述了6個常用的斷言方法:

方法用途
assertEqual(a,b)核實a == b
assertNotEqual(a,b)核實a != b
assertTrue(x)核實x為True
assertFalse(x)核實x為False
assertIn(item,list)核實item在list
assertNotIn(item,list)核實item不在list中
  • 測試AnonymyousSurvey類
    類的測試與函式的測試相似——你所做的大部分工作都是測試類中方法的行為,但存在一些不同之處.
    survey.py
class AnonymousSurvey():
    """收集匿名調查問卷的答案"""
    
    def __init__(self,question):
        """儲存一個問題,併為儲存答案做準備"""
        self.question = question
        self.responses = []

    def show_question(self):
        """顯示調查問卷"""
        print(self.question)

    def store_response(self,new_response):
        """儲存單份調查答案"""
        self.responses.append(new_response)

    def show_results(self):
        """顯示收集到的所有答卷"""
        print("Survey results:")
        for response in self.responses:
            print('- ' + response)

接下來編寫一個測試,對AnonymousSurvey類的行為的一個方面進行驗證:如果使用者面對調查問題時只提供一個答案以及提供多個答案時,程式能夠妥善將其儲存:

import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """針對AnonymousSurvey類的測試"""

    def test_store_single_response(self):
        """測試單個答案能否被正常儲存"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('English')

        self.assertIn('English',my_survey.responses)

    def test_store_single_response(self):
        """測試多個個答案能否被正常儲存"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English','Chinese','English']
        for response in responses:
            my_survey.store_response(response)

        for response in responses:
            self.assertIn(response,my_survey.responses)

unittest.main()

執行結果:

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK
  • 首先,我們依舊是要匯入unittest模組以及AnonymousSurvey類
  • 分別寫兩個測試方法對不同情形進行測試
  • 最後,兩個測試都通過了

前面的做法效果很好,但這些程式碼有重複的地方,下面將解決這一問題

  • 方法setUp()
    在前面的測試中,我們在每個測試方法都建立了一個AnonymousSurvey例項,並在每個方法都建立了答案.unittest.TestCase類中包含方法setUp,python將先執行它,再執行其它以test_打頭的方法.這樣,在編寫的每個測試方法中都可使用在方法setUp()中建立的物件.
import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """針對AnonymousSurvey類的測試"""

    def setUp(self):
        """
        建立一個調查物件和一組答案,供使用的測試方法使用
        """
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['English','Chinese','English']

    def test_store_single_response(self):
        """測試單個答案能否被正常儲存"""        
        self.my_survey.store_response(self.responses[0])

        self.assertIn(self.responses[0],self.my_survey.responses)

    def test_store_three_response(self):
        """測試多個個答案能否被正常儲存"""
        for response in self.responses:
            self.my_survey.store_response(response)

        for response in self.responses:
            self.assertIn(response,self.my_survey.responses)

unittest.main()

執行結果:

..
----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK
  • 方法setUp()做了兩件事:建立一個調查物件;建立一個答案列表.儲存這兩樣東西的變數包含字首self(即包含在屬性內),因此可以在類的任何地方使用.這讓兩個測試變得更簡單,因為它們都不用建立新的物件和答案.
  • 測試自己編寫的類時,方法setUp()讓測試方法編寫起來更簡單:可在setUp()方法中建立一系列示例並設定它們的屬性,再在測試方法中直接使用這些例項.

注意:執行測試用例時,每完成一個單元測試,python都會列印一個字元:測試通過時列印一個句點;測試引發錯誤時列印一個E;測試導致斷言失敗時列印一個F.這就是你執行測試用例時,在輸出的第一行看到的句點和字元數量各不相同的原因.如果測試用例包含很多單元測試,需要執行很長時間,就可以通過這些結果來獲悉有多少個測試通過了.

個人從書本學到的知識,作為自己學習筆記的同時,與各位朋友分享,如有不足,請多多指教

參考文獻:《Python程式設計從入門到實踐》【美】Eric Matthes 著 袁國忠 譯