天天看點

《Python程式設計:從入門到實踐》學習筆記——第11章 測試代碼前言1 測試函數2 測試類

文章目錄

  • 前言
  • 1 測試函數
    • 1.1 單元測試和測試用例
    • 1.2 可通過的測試
    • 1.3 不能通過的測試
    • 1.4 測試未通過時怎麼辦
    • 1.5 添加新測試
  • 2 測試類
    • 2.1 各種斷言方法
    • 2.2 一個要測試的類
    • 2.3 測試 AnonymousSurvey 類
    • 2.4 方法 setUp()
    • 動手試一試11-3

前言

Python初學者一枚,文章僅為個人學習記錄,便于以後檢視使用。

編寫函數或類時,還可為其編寫測試。通過測試,可确定代碼面對各種輸入都能夠按要求的那樣工作。在程式中添加新代碼時,你也可以對其進行測試,确認它們不會破壞程式既有的行為。

1 測試函數

name_function.py

def get_formatted_name(first, last):
    """Generate a neatly formatted full name"""
    full_name = first + ' ' + last
    return full_name.title()
           

1.1 單元測試和測試用例

Python标準庫中的子產品unittest提供了代碼測試工具。

單元測試用于核實函數的某個方面沒有問題;

測試用例是一組單元測試,這些單元測試一起核實函數在各種情形下的行為都符合要求。

良好的測試用例考慮到了函數可能收到的各種輸入,包含針對所有這些情形的測試。全覆寫式測試用例包含一整套單元測試,涵蓋了各種可能的函數使用方式。對于大型項目,要實作全覆寫可能很難。通常,最初隻要針對代碼的重要行為編寫測試即可,等項目被廣泛使用時再考慮全覆寫。

1.2 可通過的測試

要為函數編寫測試用例,可先導入子產品unittest以及要測試的函數,再建立一個繼承unittest.TestCase的類,并編寫一系列方法對函數行為的不同方面進行測試。

下面是一個隻包含一個方法的測試用例,它檢查函數get_formatted_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')


# 第一種
main = NamesTestCase
# main等于建立的類的名稱,即class後的名稱
'''
第二種:unittest.main(argv=['first-arg-is-ignored'], exit=False)
第三種:unittest.main(argv=['ignored', '-v'], exit=False)
'''
           

▲▲▲最後行書中原文寫unittest.main(),但會報錯:

《Python程式設計:從入門到實踐》學習筆記——第11章 測試代碼前言1 測試函數2 測試類

解決方法及原因第一頁最後一條回複(2020-02-07 16:51:13)、第二頁最後一條回複(2021-02-01 10:58:49)親測有效。

首先,我們導入了子產品unittest和要測試的函數get_formatted_ name()。

在第4行,我們建立了一個名為NamesTestCase的類,用于包含一系列針對get_formatted_name()的單元測試。你可随便給這個類命名,但最好讓它看起來與要測試的函數相關,并包含字樣Test。這個類必須繼承unittest.TestCase類,這樣Python才知道如何運作你編寫的測試。

NamesTestCase隻包含一個方法,用于測試get_formatted_name()的一個方面。我們将這個方法命名為test_first_last_name(),因為我們要核實的是隻有名和姓的姓名能否被正确地格式化。

我們運作test_name_function.py時,所有以test_打頭的方法都将自動運作。在這個方法中,我們調用了要測試的函數,并存儲了要測試的傳回值。在這個示例中,我們使用實參’janis’和’joplin’調用get_formatted_name(),并将結果存儲到變量formatted_name中(見第9行)。

在第10行,我們使用了unittest類最有用的功能之一:一個斷言方法。斷言方法用來核實得到的結果是否與期望的結果一緻。

在這裡,我們知道get_formatted_name()應傳回這樣的姓名,即名和姓的首字母為大寫,且它們之間有一個空格,是以我們期望formatted_name的值為Janis Joplin。為檢查是否确實如此,我們調用unittest的方法assertEqual(),并向它傳遞formatted_ name和’Janis Joplin’。

代碼行self.assertEqual(formatted_name, ‘Janis Joplin’)的意思是說:“将formatted_name的值同字元串’Janis Joplin’進行比較,如果它們相等,就萬事大吉,如果它們不相等,跟我說一聲!”

代碼行unittest.main()讓Python運作這個檔案中的測試。

書中運作test_name_function.py時,得到的輸出:

《Python程式設計:從入門到實踐》學習筆記——第11章 測試代碼前言1 測試函數2 測試類
《Python程式設計:從入門到實踐》學習筆記——第11章 測試代碼前言1 測試函數2 測試類

1.3 不能通過的測試

新版本函數get_formatted_name(),它要求通過一個實參指定中間名:

name_function.py

def get_formatted_name(first, middle, last):
    """Generate a neatly formatted full name"""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()
           

運作程式test_name_function.py時,輸出如下:

最後一行按第一種方式

《Python程式設計:從入門到實踐》學習筆記——第11章 測試代碼前言1 測試函數2 測試類

最後一行按第二種方式

《Python程式設計:從入門到實踐》學習筆記——第11章 測試代碼前言1 測試函數2 測試類

書中原文:

《Python程式設計:從入門到實踐》學習筆記——第11章 測試代碼前言1 測試函數2 測試類

1.4 測試未通過時怎麼辦

如果你檢查的條件沒錯,測試通過了意味着函數的行為是對的,而測試未通過意味着你編寫的新代碼有錯。是以,測試未通過時,不要修改測試,而應修複導緻測試不能通過的代碼:檢查剛對函數所做的修改,找出導緻函數行為不符合預期的修改。

修改name_function.py

def get_formatted_name(first, last, middle=''):
    """Generate a neatly formatted full name"""
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()
           

1.5 添加新測試

确定get_formatted_name()又能正确地處理簡單的姓名後,我們再編寫一個測試,用于測試包含中間名的姓名。為此,我們在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_last_middle_name(self):
        """能夠正确地處理像Wolfgang Amadeus Mozart這樣的姓名嗎?"""
        formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')


# 第一種
main = NamesTestCase
# main等于建立的類的名稱,即class後的名稱
'''
第二種:unittest.main(argv=['first-arg-is-ignored'], exit=False)
第三種:unittest.main(argv=['ignored', '-v'], exit=False)
'''
           

在TestCase類中使用很長的方法名是可以的;這些方法的名稱必須是描述性的,這才能讓你明白測試未通過時的輸出;這些方法由Python自動調用,你根本不用編寫調用它們的代碼。

2 測試類

2.1 各種斷言方法

Python在unittest.TestCase類中提供了很多斷言方法。斷言方法檢查你認為應該滿足的條件是否确實滿足。如果該條件确實滿足,你對程式行為的假設就得到了确認,你就可以确信其中沒有錯誤。如果你認為應該滿足的條件實際上并不滿足,Python将引發異常。

表11-1描述了6個常用的斷言方法。使用這些方法可核實傳回的值等于或不等于預期的值、傳回的值為True或False、傳回的值在清單中或不在清單中。

你隻能在繼承unittest.TestCase的類中使用這些方法。

《Python程式設計:從入門到實踐》學習筆記——第11章 測試代碼前言1 測試函數2 測試類

2.2 一個要測試的類

類的測試與函數的測試相似——你所做的大部分工作都是測試類中方法的行為,但存在一些不同之處。

示例:

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)
           

這個類首先存儲了一個你指定的調查問題(見第4行),并建立了一個空清單,用于存儲答案。這個類包含列印調查問題的方法(見第9行)、在答案清單中添加新答案的方法(見第13行)以及将存儲在清單中的答案都列印出來的方法(見第17行)。要建立這個類的執行個體,隻需提供一個問題即可。有了表示調查的執行個體後,就可使用show_question()來顯示其中的問題,可使用store_response()來存儲答案,并使用show_results()來顯示調查結果。

為證明AnonymousSurvey類能夠正确地工作,我們來編寫一個使用它的程式:

from survey import AnonymousSurvey

# 定義一個問題,并建立一個表示調查的AnonymousSurvey對象
question = 'What language did you first learn to speak?'
my_survey = AnonymousSurvey(question)

# 顯示問題并存儲答案
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
    response = input("Language: ")
    if response == 'q':
        break
    my_survey.store_response(response)

# 顯示調查結果
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()
           

輸出:

《Python程式設計:從入門到實踐》學習筆記——第11章 測試代碼前言1 測試函數2 測試類

書中survey.py中,第11行和第20行原文錯誤,為print(question)和for response in responses:

2.3 測試 AnonymousSurvey 類

下面來編寫一個測試,對AnonymousSurvey類的行為的一個方面進行驗證:如果使用者面對調查問題時隻提供了一個答案,這個答案也能被妥善地存儲。為此,我們将在這個答案被存儲後,使用方法assertIn()來核實它包含在答案清單中:

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)


main = TestAnonymousSurvey
           

我們首先導入了子產品unittest以及要測試的類AnonymousSurvey。

我們将測試用例命名為TestAnonymousSurvey,它也繼承了unittest.TestCase(見第4行)。第一個測試方法驗證調查問題的單個答案被存儲後,會包含在調查結果清單中。

要測試類的行為,需要建立其執行個體。在第10行,我們使用問題"What language did you first learn to speak?"建立了一個名為my_survey的執行個體,然後使用方法store_response()存儲了單個答案English。

接下來,我們檢查English是否包含在清單my_survey.responses中,以核實這個答案是否被妥善地存儲了(見第13行)。

當我們運作test_survey.py時,測試通過了。

下面來核實使用者提供三個答案時,它們也将被妥善地存儲。為此,我們在TestAnonymousSurvey中再添加一個方法:

import unittest
from survey import AnonymousSurvey


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

    def test_store_single_response(self):
        """測試單個答案會被妥善地存儲"""
        --snip--
    
    def test_store_three_responses(self):
        """測試三個答案會被妥善地存儲"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English', 'Spanish', 'Mandarin']
        for response in responses:
            my_survey.store_response(response)

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


main = TestAnonymousSurvey
           

前述做法的效果很好,但這些測試有些重複的地方。下面使用unittest的另一項功能來提高它們的效率。

2.4 方法 setUp()

在前面的test_survey.py中,我們在每個測試方法中都建立了一個AnonymousSurvey執行個體,并在每個方法中都建立了答案。

unittest.TestCase類包含方法setUp(),讓我們隻需建立這些對象一次,并在每個測試方法中使用它們。如果你在TestCase類中包含了方法setUp(),Python将先運作它,再運作各個以test_打頭的方法。這樣,在你編寫的每個測試方法中都可使用在方法setUp()中建立的對象了。

下面使用setUp()來建立一個調查對象和一組答案,供方法test_store_single_response()和test_store_three_responses()使用:

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', 'Spanish', 'Mandarin']

    def test_store_single_responses(self):
        self.my_survey.store_response(self.responses[0])
        self.assertIn(self.responses[0], self.my_survey.responses)

    def test_store_three_responses(self):
        """測試三個答案會被妥善地存儲"""
        for response in self.responses:
            self.my_survey.store_response(response)
        for response in self.responses:
            self.assertIn(response, self.my_survey.responses)


main = TestAnonymousSurvey
           

方法setUp()做了兩件事情:建立一個調查對象(見第11行);建立一個答案清單(見第12行)。存儲這兩樣東西的變量名包含字首self(即存儲在屬性中),是以可在這個類的任何地方使用。

方法test_store_three_response()核 實 self.responses 中的第一個答案 ——self.responses[0]—— 被妥善地存儲,而方法test_store_three_response()核實self.responses中的全部三個答案都被妥善地存儲。

再次運作test_survey.py時,這兩個測試也都通過了。修改代碼以接受多個答案後,可運作這些測試,确認存儲單個答案或一系列答案的行為未受影響。

測試自己編寫的類時,方法setUp()讓測試方法編寫起來更容易:可在setUp()方法中建立一系列執行個體并設定它們的屬性,再在測試方法中直接使用這些執行個體。相比于在每個測試方法中都建立執行個體并設定其屬性,這要容易得多。

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

動手試一試11-3

雇員:編寫一個名為 Employee 的類,其方法__init__()接受名、姓和年薪,并将它們都存儲在屬性中。編寫一個名為 give_raise()的方法,它預設将年薪增加 5000美元,但也能夠接受其他的年薪增加量。

為 Employee 編寫一個測試用例,其中包含兩個測試方法:test_give_default_ raise()和 test_give_custom_raise()。使用方法 setUp(),以免在每個測試方法中都建立新的雇員執行個體。運作這個測試用例,确認兩個測試都通過了。

employee.py

class Employee():

    def __init__(self, first, last, salary):
        self.first = first
        self.last = last
        self.salary = salary

    def give_raise(self, addsalary=5000):
        self.salary += addsalary
           

test_employee.py

import unittest
from employee import Employee


class TestEmployee(unittest.TestCase):

    def setUp(self):
        self.formatted_default = Employee('Edward', 'Elric', 1000)

    def test_give_default_raise(self):
        self.formatted_default.give_raise()
        self.assertEqual(self.formatted_default.salary, 6000)

    def test_give_custom_raise(self):
        self.formatted_default.give_raise(10000)
        self.assertEqual(self.formatted_default.salary, 11000)


main = TestEmployee