Python學習之路10-測試程式碼

VPointer發表於2019-02-16

《Python程式設計:從入門到實踐》筆記。

本章主要學習如何使用Python標準庫中的unittest模組對程式碼進行簡單的測試。

1. 前言

作為初學者,並非必須為你嘗試的所有專案編寫測試;但參與工作量較大的專案時,你應對自己編寫的函式和類的重要行為進行測試。這樣你就能夠更加確定自己所做的工作不會破壞專案的其他部分,你就能夠隨心所欲地改進即有程式碼。如果不小心破壞了原來的功能,你馬上就會知道,從而能夠輕鬆地修復問題。相比於等到Bug出現後再去改,在測試未通過時採取措施要容易得多。而且,如果你想要分享你的專案,有測試的程式碼更容易讓人接受。

2. 測試函式

2.1 一個能通過的測試

以下是一個將使用者輸入的姓與名拼接的函式:

# name_function.py
def get_formatted_name(first, last):
    """返回一個整潔的完整姓名"""
    full_name = first + " " + last
    return full_name.title()


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

        formatted_name = get_formatted_name(first, last)
        print("\nNeatly formatted name: " + formatted_name + ".")
複製程式碼

當然你也可以將if語句下面的程式碼單獨放在一個檔案中,並在該檔案開頭帶入get_formatted_name()函式。

if __name__ == "__main__"的補充:

在Python中,模組就是物件,所有模組都有一個內建屬性__name__,當該模組被匯入時,該模組的__name__屬性會被置為模組名,當直接執行該模組,或者說直接執行該檔案時,該屬性就會使用預設值"__main__",可以用一句經典的話總結這個用法:

Make a script both importable and executable.

if語句下面的程式碼相當於對上面的函式的測試,不過這樣的測試每次都需要我們自己輸入資料,並自己根據結果判斷程式碼是否工作正常,如果程式碼稍微多一點,稍微複雜一點,這樣的測試方法將會很繁瑣,所以,我們使用unittest模組了測試程式碼。

# 程式碼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
複製程式碼

這裡先明確兩個概念:

單元測試:用於核實函式在某個方面沒有問題

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

也就是說,你可以將上述程式碼中的test_first_last_name看做單元測試,而將NamesTestCase看做測試用例。

一般測試檔案單獨放在一個資料夾中,也可以將測試都放在一個檔案中。

為函式編寫測試用例,可先匯入unittest模組和要測試的函式,再建立一個繼承unittest.TestCase的類,並編寫一系列方法對函式行為的不同方面進行測試。在測試用,我們使用斷言self.assertEqual()(並不是只有這一個斷言函式)來判斷結果與期望是否相同。在測試類中的每一個測試方法都必須以test_開頭,否則將不會被認定是一個單元測試。最後我們通過unittest.main()來執行這個檔案中的所有測試。當測試通過時,結果中會先輸出一個實心句點,輸出幾個句點表示通過了幾個單元測試,然後輸出單元測試數目,最後輸出OK

2.2 一個不能通過的測試

外國人的名字還有中間名,以上程式碼並未考慮這個情況。我們通過將上述程式碼改成含有中間名的版本來演示測試不通過的情況:

# 程式碼:
def get_formatted_name(first, middle, last):
    """返回一個整潔的完整姓名"""
    full_name = first + " " + middle + " " + last
    return full_name.title()

# 其餘程式碼均不變

# 執行上面測試程式碼後的結果:
E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
能夠正確地處理像Janis Joplin這樣的名字嗎?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_name_function.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.001s

FAILED (errors=1)
複製程式碼

第一行輸出了一個字母Etraceback指出缺少了引數。如果你檢查的條件沒錯,測試通過了意味著函式的行為是對的,而測試未通過意味著你編寫的新程式碼有錯。因此,測試未通過時,不是去修改測試程式碼,而失去修改你編寫的程式碼。

2.3 新增新測試

以下我們將上述的get_formatted_name()函式修改為能自動處理中間名的函式,並在測試檔案中新增一個單元測試:

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

# test_name_function.py
import unittest
from chapter11 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")

unittest.main()

# 結果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
複製程式碼

3. 測試類

前面講的都是對函式的測試,這裡我們開始對類的測試。在測試之前,先介紹幾種常用的斷言方法:

方法 用途
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中

下面建立一個匿名調查類:

# 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)
複製程式碼

以下是對該類的測試程式碼:

# test_survey.py
import unittest
from chapter11 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_response(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)

unittest.main()

# 結果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
複製程式碼

這裡的setUp()方法相當於普通函式的__init__()方法,用於初始化這個測試類,減少重複程式碼,比如,如果不用setUp()方法,那麼question變數在每個測試函式中都要宣告一次,十分麻煩低效。你過測試類中包含了setUp()方法,Python將先執行它,再執行各個以test_開頭的方法。

至此,Python的基礎部分大致結束,後面將是專案部分,以後可能還會對基礎部分進行補充。


迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~

Python學習之路10-測試程式碼

相關文章