《Python編程:從入門到實踐》第11章 筆記

11 測試代碼

  編寫函數或類時,還可爲其編寫測試。經過測試,可肯定代碼面對各類輸入都可以按要求的那樣工做。在程序中添加新代碼時,你也能夠對其進行測試,確認它們不會破壞程序既有的行爲。
  在本章中,你將學習如何使用Python模塊unittest中的工具來測試代碼。你將學習編寫測試用例,覈實一系列輸入都將獲得預期的輸出。你將看到測試經過了是什麼樣子,測試未經過又是什麼樣子,還將知道測試未經過如何有助於改進代碼。你將學習如何測試函數和類,並將知道該爲項目編寫多少個測試。 python

"""11.1 測試函數"""
""" 下面是一個簡單的函數,它接受名和姓並返回整潔的姓名: """
"""name_function.py"""

def get_formatted_name(first, last): 
    """Generate a neatly formatted full name.""" 
    full_name = first + ' ' + last 
    return full_name.title()
""" 爲覈實get_formatted_name()像指望的那樣工做,咱們來編寫一個 使用這個函數的程序。程序names.py讓用戶輸入名和姓,並顯示整潔的全名: """
"""names.py"""

from name_function import get_formatted_name

print "Enter 'q' at any time to quit."
while True:
    first = raw_input("\nPlease give me a first name: ")
    if first == 'q':
        break

    last = raw_input("Please give me a last name: ")
    if last == 'q':
        break

    formatted_name = get_formatted_name(first, last)
    print("\tNeatly formatted name: " + formatted_name + '.')
Enter 'q' at any time to quit.

Please give me a first name: janis
Please give me a last name: joplin
    Neatly formatted name: Janis Joplin.

Please give me a first name: bob
Please give me a last name: dylan
    Neatly formatted name: Bob Dylan.

Please give me a first name: q

  從上述輸出可知,合併獲得的姓名正確無誤。如今假設咱們要修改get_formatted_name(),使其還可以處理中間名。這樣作時,咱們要確保不破壞這個函數處理只有名和姓的姓名的方式。爲此,咱們能夠在每次修改get_formatted_name()後都進行測試:運行程序names.py,並輸入像Janis Joplin這樣的姓名,但這太煩瑣了。
  所幸Python提供了一種自動測試函數輸出的高效方式。假若咱們對get_formatted_name()進行自動測試,就能始終信心滿滿,確信給這個函數提供咱們測試過的姓名時,它都能正確地工做。 程序員

11.1.1 單元測試和測試用例

  Python標準庫中的模塊unittest提供了代碼測試工具。
  單元測試用於覈實函數的某個方面沒有問題;測試用例是一組單元測試,這些單元測試一塊兒覈實函數在各類情形下的行爲都符合要求。
  良好的測試用例考慮到了函數可能收到的各類輸入,包含針對全部這些情形的測試。全覆蓋式測試用例包含一整套單元測試,涵蓋了各類可能的函數使用方式。對於大型項目,要實現全覆蓋可能很難。一般,最初只要針對代碼的重要行爲編寫測試便可,等項目被普遍使用時再考慮全覆蓋。 markdown

11.1.2 可經過的測試

  要爲函數編寫測試用例,可先導入模塊unittest以及要測試的函數,再建立一個繼承unittest.TestCase的類,並編寫一系列方法(須要以test或Test開頭)對函數行爲的不一樣方面進行測試。 下面是一個只包含一個方法的測試用例,它檢查函數get_formatted_name()在給定名和姓時可否正確地工做: app

"""test_name_function.py"""

import unittest
#from name_function import get_formatted_name

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

class NamesTestCase(unittest.TestCase):  # 這個類必須繼承unittest.TestCase類
    """測試name_function.py"""   

    def test_first_last_name(self):
        formatted_name = get_formatted_name('janis', 'joplin')

        # ①
        self.assertEqual(formatted_name, 'Janis Joplin')

if __name__ == '__main__':  
# unittest.main()
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
.

Janis Joplin



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

OK

①處,咱們使用了unittest類最有用的功能之一:一個斷言方法。斷言方法用來覈實獲得的結果是否與指望的結果一致。函數

  在這裏,咱們指望formatted_name的值爲"Janis Joplin"。爲檢查是否確實如此,咱們調用unittest的方法assertEqual(),並向它傳遞formatted_name"Janis Joplin"進行比較。代碼行unittest.main()讓Python運行這個文件中的測試。工具

說明:單元測試

  書中原代碼在本地能夠運行,可是在jupyter notebook中運行報錯」AttributeError: ‘module’ object has no attribute」,看到Stack Overflow上的問答,參考修改後能夠在jupyter notebook中運行。學習

unittest.main(argv=['first-arg-is-ignored'], exit=False)測試

unittest.main(argv=['ignored', '-v'], exit=False)ui

1.1.3 不能經過的測試

  修改get_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

"""test_name_function.py"""

import unittest
#from name_function import get_formatted_name

def get_formatted_name(first, middle, last): 
    """生成整潔的姓名""" 
    full_name = first + ' ' + middle + ' ' + last 
    return full_name.title() 

class NamesTestCase(unittest.TestCase):  # 這個類必須繼承unittest.TestCase類
    """測試name_function.py"""   

    def test_first_last_name(self):
        formatted_name = get_formatted_name('janis', 'joplin')

        self.assertEqual(formatted_name, 'Janis Joplin')

if __name__ == '__main__':  
# unittest.main()
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-22-c6f1d3890843>", line 15, in test_first_last_name
    formatted_name = get_formatted_name('janis', 'joplin')
TypeError: get_formatted_name() takes exactly 3 arguments (2 given)

----------------------------------------------------------------------
Ran 1 test in 0.041s

FAILED (errors=1)

11.1.4 測試未經過時怎麼辦

  測試未經過時怎麼辦呢?若是你檢查的條件沒錯,測試經過了意味着函數的行爲是對的,而測試未經過意味着你編寫的新代碼有錯。所以,測試未經過時,不要修改測試,而應修復致使測試不能經過的代碼:檢查剛對函數所作的修改,找出致使函數行爲不符合預期的修改。
  在這個示例中,get_formatted_name()之前只須要兩個實參——名和姓,但如今它要求提供名、中間名和姓。就這裏而言,最佳的選擇是讓中間名變爲可選的。這樣作後,使用相似於Janis Joplin的姓名 進 行 測 試 時 , 測 試 就 會 通 過 了 , 同 時 這 個 函 數 還 能 接 受 中 間 名 。 下 面 來 修 改get_formatted_name(),將中間名設置爲可選的,而後再次運行這個測試用例。若是經過了,咱們接着確認這個函數可以妥善地處理中間名。

  將中間名設置爲可選的,可在函數定義中將形參middle移到形參列表末尾,並將其默認值指定爲一個空字符串。咱們還要添加一個if測試,以便根據是否提供了中間名相應地建立姓名:

"""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()

  再次運行test_name_function.py:

"""test_name_function.py"""

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):  # 這個類必須繼承unittest.TestCase類
    """測試name_function.py"""   

    def test_first_last_name(self):
        formatted_name = get_formatted_name('janis', 'joplin')
        print formatted_name
        self.assertEqual(formatted_name, 'Janis Joplin')

if __name__ == '__main__':  
# unittest.main()
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
.

Janis Joplin



----------------------------------------------------------------------
Ran 1 test in 0.003s

OK

11.1.5 添加新測試

  肯定get_formatted_name()又能正確地處理簡單的姓名後,咱們再編寫一個測試,用於測試包含中間名的姓名。爲此,咱們在NamesTestCase類中再添加一個方法:

"""test_name_function.py"""

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):  # 這個類必須繼承unittest.TestCase類
    """測試name_function.py"""   

    def test_first_last_name(self):
        formatted_name = get_formatted_name('janis', 'joplin')
        print formatted_name
        self.assertEqual(formatted_name, 'Janis Joplin')

    def test_first_last_middle_name(self):
        formatted_name = get_formatted_name(
            'wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

if __name__ == '__main__':  
# unittest.main()
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
...

Janis Joplin



----------------------------------------------------------------------
Ran 3 tests in 0.007s

OK

  咱們將這個方法命名爲test_first_last_middle_name()。方法名必須以test_打頭,這樣它纔會在咱們運行test_name_function.py時自動運行。這個方法名清楚地指出了它測試的是get_formatted_name()的哪一個行爲,這樣,若是該測試未經過,咱們就會立刻知道受影響的是哪一種類型的姓名。在TestCase類中使用很長的方法名是能夠的;這些方法的名稱必須是描述性的,這才能讓你明白測試未經過時的輸出;這些方法由Python自動調用,你根本不用編寫調用它們的代碼。
  爲測試函數get_formatted_name(),咱們使用名、姓和中間名調用它,再使用assertEqual()檢查返回的姓名是否與預期的姓名(名、中間名和姓)一致。

習題11-1 城市和國家

  編寫一個函數,它接受兩個形參:一個城市名和一個國家名。這個函數返回一個格式爲 City, Country 的字符串,如 Santiago, Chile 。將這個函數存儲在一個名爲 city_functions.py 的模塊中。
  建立一個名爲 test_cities.py 的程序,對剛編寫的函數進行測試(別忘了,你須要導入模塊 unittest 以及要測試的函數)。編寫一個名爲 test_city_country()的方法,覈實使用相似於 ‘santiago’ 和 ‘chile’ 這樣的值來調用前述函數時,獲得的字符串是正確的。
運行 test_cities.py,確認測試 test_city_country()經過了。

"test_cities.py"

import unittest
from  city_functions import city_country

class CityCountryTest(unittest.TestCase):

    def test_city_country(self):
        formatted_string = city_country('santiago', 'chile')
        self.assertEqual(formatted_string, 'Santiago, Chile')

if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
...

Janis Joplin



----------------------------------------------------------------------
Ran 3 tests in 0.007s

OK

習題11–2 人口數量

  修改前面的函數,使其包含第三個必不可少的形參 population,並返回一個格式爲 City, Country – population xxx 的字符串,如 Santiago, Chile – population 5000000。運行 test_cities.py,確認測試 test_city_country()未經過。
  修改上述函數,將形參population 設置爲可選的。再次運行 test_cities.py,確認測試 test_city_country()又經過了。
  再編寫一個名爲 test_city_country_population()的測試,覈實可使用相似於 ‘santiago’、’chile’ 和 ‘population=5000000’ 這樣的值來調用這個函數。再次運行test_cities.py,確認測試 test_city_country_population()經過了。

"test_cities.py"
"加一個形參population"

import unittest
from  city_functions import city_country

class CityCountryTest(unittest.TestCase):

    def test_city_country(self):
        formatted_string = city_country('santiago', 'chile')
        self.assertEqual(formatted_string, 'Santiago, Chile')

if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
E
======================================================================
ERROR: test_city_country (__main__.CityCountryTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-1-40099b039d57>", line 9, in test_city_country
    formatted_string = city_country('santiago', 'chile')
TypeError: city_country() takes exactly 3 arguments (2 given)

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

FAILED (errors=1)
"test_cities.py"
"形參可選"

import unittest
from  city_functions import city_country

class CityCountryTest(unittest.TestCase):

    def test_city_country(self):
        formatted_string = city_country('santiago', 'chile')
        self.assertEqual(formatted_string, 'Santiago, Chile')

if __name__ == '__main__':
    unittest.main(argv=['first_arg-is-ignored'], exit=False)
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK
"test_cities.py"
"測試人口"

import unittest
from  city_functions import city_country

class CityCountryTest(unittest.TestCase):

    def test_city_country(self):
        formatted_string = city_country('santiago', 'chile')
        self.assertEqual(formatted_string, 'Santiago, Chile')

    def test_city_country_population(self):
        formatted_string = city_country('santiago', 'chile', 5000000)
        self.assertEqual(formatted_string, 'Santiago, Chile - population 5000000')

if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
..
----------------------------------------------------------------------
Ran 2 tests in 0.005s

OK

11.2 測試類

11.2.1 各類斷言方法

  Python在unittest.TestCase類中提供了不少斷言方法。表中描述了6個經常使用的斷言方法。使用這些方法可覈實返回的值等於或不等於預期的值、返回的值爲TrueFalse、返回的值在列表中或不在列表中。你只能在繼承unittest.TestCase的類中使用這些方法。

方法 用途
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中
"""11.2.2 一個要測試的類 """

"""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類的程序"""

"""language_survey.py"""

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 = raw_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()
What language did you first learn to speak?
Enter 'q' at any time to quit.

Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q

Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
- Mandarin
"""11.2.3 測試 AnonymousSurvey 類 """

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymousSurvey."""


    def test_store_single_response(self):
        """Test that a single response is stored properly."""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question) 
        my_survey.store_response('English') 

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


if __name__ == '__main__':           
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

  這很好,但只能收集一個答案的調查用途不大。下面來覈實用戶提供三個答案時,它們也將被妥善地存儲。爲此,咱們在TestAnonymousSurvey中再添加一個方法:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymousSurvey."""


    def test_store_single_response(self):
        """Test that a single response is stored properly."""
        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_three_responses(self):
        """Test that three individual responses are stored properly."""
        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)         



if __name__ == '__main__':           
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
..
----------------------------------------------------------------------
Ran 2 tests in 0.004s

OK

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

11.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):
    """Tests for the class AnonymousSurvey."""

    def setUp(self):
        """ Create a survey and a set of responses for use in all test methods. """
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['English', 'Spanish', 'Mandarin']


    def test_store_single_response(self):
        """Test that a single response is stored properly."""
        self.my_survey.store_response(self.responses[0])
        self.assertIn(self.responses[0], self.my_survey.responses)


    def test_store_three_responses(self):
        """Test that three individual responses are stored properly."""
        for response in self.responses:
            self.my_survey.store_response(response)
        for response in self.responses:
            self.assertIn(response, self.my_survey.responses)


if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
..
----------------------------------------------------------------------
Ran 2 tests in 0.003s

OK

  方法setUp()作了兩件事情:建立一個調查對象;建立一個答案列表。存儲這兩樣東西的變量名包含前綴self(即存儲在屬性中),所以可在這個類的任何地方使用。這讓兩個測試方法都更簡單,由於它們都不用建立調查對象和答案。方法test_store_single_response()核 實 self.responses 中 的 第 一 個 答 案 ——self.responses[0]—— 被 妥 善 地 存 儲 , 而 方 法test_store_three_response()覈實self.responses中的所有三個答案都被妥善地存儲。
  再次運行test_survey.py時,這兩個測試也都經過了。若是要擴展AnonymousSurvey,使其容許每位用戶輸入多個答案,這些測試將頗有用。修改代碼以接受多個答案後,可運行這些測試,確認存儲單個答案或一系列答案的行爲未受影響。
  測試本身編寫的類時,方法setUp()讓測試方法編寫起來更容易:可在setUp()方法中建立一系列實例並設置它們的屬性,再在測試方法中直接使用這些實例。相比於在每一個測試方法中都建立實例並設置其屬性,這要容易得多。

注意
  運行測試用例時,每完成一個單元測試,Python都打印一個字符:測試經過時打印一個句點;測試引起錯誤時打印一個E;測試致使斷言失敗時打印一個F。這就是你運行測試用例時,在輸出的第一行中看到的句點和字符數量各不相同的緣由。若是測試用例包含不少單元測試,須要運行很長時間,就可經過觀察這些結果來獲悉有多少個測試經過了。

11-3 僱員:
  編寫一個名爲 Employee 的類,其方法__init__()接受名、姓和年薪,並將它們都存儲在屬性中。編寫一個名爲give_raise()的方法,它默認將年薪增長 5000美圓,但也可以接受其餘的年薪增長量。
  爲 Employee 編寫一個測試用例,其中包含兩個測試方法:test_give_default_raise()test_give_custom_raise()。使用方法 setUp(),以避免在每一個測試方法中都建立新的僱員實例。運行這個測試用例,確認兩個測試都經過了。

class Employee():
    def __init__(self,last_name, first_name, salary=10000 ):
        self.last_name = last_name
        self.first_name = first_name
        self.salary = salary

    def give_raise(self,added=5000):
        self.salary += added
        return added
import unittest
from EmployeeFile import Employee

class TestEmployeeRaise(unittest.TestCase):

    def setUp(self):
        self.test1 = Employee('Tom', 'Smith')
        self.test2 = Employee('Tom', 'Smith',3000)


    def test_give_default_raise(self):
        self.salary1 = self.test1.give_raise()
        self.assertEqual(str(self.salary1), '5000')


    def test_give_custom_raise(self):
        self.salary2 = self.test2.give_raise(3000)
        self.assertEqual(str(self.salary2), '3000')


if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
..
----------------------------------------------------------------------
Ran 2 tests in 0.003s

OK

11.3 小結

  測試是不少初學者都不熟悉的主題。做爲初學者,並不是必須爲你嘗試的全部項目編寫測試;但參與工做量較大的項目時,你應對本身編寫的函數和類的重要行爲進行測試。這樣你就可以更加肯定本身所作的工做不會破壞項目的其餘部分,你就可以爲所欲爲地改進既有代碼了。若是不當心破壞了原來的功能,你立刻就會知道,從而可以輕鬆地修復問題。相比於等到不滿意的用戶報告bug後再採起措施,在測試未經過時採起措施要容易得多。   若是你在項目中包含了初步測試,其餘程序員將更敬佩你,他們將可以更駕輕就熟地嘗試使用你編寫的代碼,也更願意與你合做開發項目。若是你要跟其餘程序員開發的項目共享代碼,就必須證實你編寫的代碼經過了既有測試,一般還須要爲你添加的新行爲編寫測試。   請經過多開展測試來熟悉代碼測試過程。對於本身編寫的函數和類,請編寫針對其重要行爲的測試,但在項目早期,不要試圖去編寫全覆蓋的測試用例,除非有充分的理由這樣作。

相關文章
相關標籤/搜索