1. 程式人生 > >第九章:除錯與測試

第九章:除錯與測試

文章目錄

第一節:斷言與單元測試

斷言

格式: assert +【條件表示式】
如果表示式成立,則程式能夠正常向下執行,沒有輸出任何內容,否則丟擲**AssertionError**
使用斷言,可以簡易迅速地對流程結果進行測試,預測結果是否相符;

例:

# 待測函式
def add(a, b):
    return a + b

# 使用斷言進行測試:
assert add(3,
4) == 7

執行結果:
系統沒有報錯,斷言是正確的,使用add這個函式得到的結果與預期相符

例:

# 待測函式
def add(a, b):
    return a * b

# 使用斷言進行測試:
assert add(3, 4) == 7

這時系統丟擲了AssertionError,說明斷言是錯誤的,待測函式add的結果於預期並不一致

單元測試

所謂單元,指的是可測試物件的最小單位,通常指一個函式。單元測試只是對工程中的程式碼單元(通常細化到每個函式)進行正確性驗證的工作;最常用的測試
對自己寫的模組和類進行必要的單元測試,保證嚴謹正確,既是良好的開發習慣,也是一種規範;
Python標準庫中單元測試的模組是 unittest

import unittest

測試用例

【要點】
測試用例類必須繼承unittest.TestCase
具體的測試項函式必須以testxxx來命名
setUp()方法會在每個測試項執行前呼叫,如有必要的初始化工作可以通過覆寫該方法來實現
tearDown()方法會在每個測試項結束後呼叫,如有善後工作可以通過覆寫該方法來實現
在整個測試用例中,還有兩個類,setUpClass(cls)tearDowwnClass(cls)也會在測試用例的執行前後各呼叫一次
在具體的測試項函式中,使用TestCase的assertXXX系列函式預言結果
如果程式執行結果與預測的一致,測該單項測試通過,否則不通過

【幾個常用斷言方法】
assertFalse(self,expr,msg) —— 斷言正確
assertTrue(self,expr,msg) —— 斷言錯誤
assertEqual(self,expr,msg) —— 斷言相等

【注意】
測試與游標有關,當游標在某函式內時,執行的是此函式的測試,當想測試所有,需把游標放在unittest.main()區域

例:

import unittest

# 待測的工具類
class MathUtil:
    # a,b相加
    def sum(self, a, b):
        return a + b

    # a,b相減
    def sub(self, a, b):
        return a - b

    # 判斷a是否大於b
    def gt(self, a, b):
        return a > b

# 測試用例類,必須繼承於unittest.TestCase
class MathUtilTest(unittest.TestCase):
    '''MathUtil工具類測試用例'''

    # 測試項初始化方法
    def setUp(self):
        print("MathUtilTest setUp,測試項正在初始化...")
        # 建立例項(用於準備資料)
        self.mu = MathUtil()

    # 測試項結束時呼叫
    def tearDown(self):
    	del self.mu    # 消毀資料
        print("MathUtilTest tearDown,測試項已結束")

    '''
    一系列的測試方法,都必須以testxxx命名
    '''

    # 測試MathUtil的sum方法
    def testSum(self):
        print("正在測試Sum方法...")
        # 斷言相等
        self.assertEqual(self.mu.sum(3, 4), 7,"Sum函式測試失敗!!!")

    # 測試MathUtil的sub方法
    def testSub(self):
        print("正在測試Sub方法...")
        # 斷言相等
        self.assertEqual(self.mu.sub(3, 4), 1, "Sub函式測試失敗!!!")

    # 測試MathUtil的gt方法
    def testGt(self):
        print("正在測試Others方法...")
        # 斷言真假
        self.assertTrue(self.mu.gt(5, 4))
        self.assertFalse(self.mu.gt(4, 5))

        # 斷言丟擲異常
        with self.assertRaises(TypeError):
            self.mu.sum(1, "2")


# 執行單元測試
if __name__ == '__main__':
    # 運行當前模組中的所有測試用例
    unittest.main()

正常執行結果:
在這裡插入圖片描述
非正常執行結果:
在這裡插入圖片描述

測試套件

測試套件,用於單獨測試某個方法,而不必整個測式用例進行測試,相對靈活一些
unittest.main()方法會執行當前模組中的所有測試用例類中的所有測試項,這顯得不太靈活;
unittest.TestSuite類是一個測試用例容器,可以按需新增測試用例於其中,使得單元測試既可以批量進行,又可以自主增減測試專案;

例:

import unittest
from unittest.runner import TextTestRunner

# 待測的工具類
class MathUtil:...

# 測試用例類,必須繼承於unittest.TestCase
class MathUtilTest(unittest.TestCase):...
class MathUtilTest2(unittest.TestCase):...
class MathUtilTest2(unittest.TestCase):...

# 執行單元測試
if __name__ == '__main__':
    # 運行當前模組中的所有測試用例
    # unittest.main()
    
    # 定義一個測試套件
    suite = unittest.TestSuite()

    # 往測試套件裡新增用例類下的所有測試項
    suite.addTest(unittest.makeSuite(MathUtilTest))
    suite.addTest(unittest.makeSuite(MathUtilTest2))

    # 執行測試套件
    runner = TextTestRunner()  # 執行器
    ret = runner.run(suite)
    print(ret)

第二節:文件測試與DEBUG

文件測試

文件指的就是Python模組,文件測試就是對一個py檔案進行整體的測試,是一種簡單粗暴的測試方式;
文件測試中的測試程式碼是以註釋的形式寫在文件中;
通過標準庫API來觸發文件測試:doctest.testmod(target_module)

例:有一個待測試的模組uut.py

'''
文件測試指令碼

#預測加法的結果
>>> add(3,4)
7

#預測減法的結果
>>> sub(3,4)
-1

#預測冪的結果
>>> power(3,4)
81
'''

# 正確的加法函式
def add(a,b):
    return a + b

# 正確的加法函式
def sub(a,b):
    return a - b

# 錯誤的冪函式
def power(a,b):
    return a ** b - 1

開始對目標模組進行測試:

#引入文件測試模組
import doctest
# 引入要進行測試的目標模組uut
import uut

if __name__ == '__main__':
    #對uut進行文件測試
    doctest.testmod(uut)

測試結果:
在這裡插入圖片描述

DEBUG

【什麼是DEBUG】
DEBUG是指對程式的執行過程進行逐行逐步除錯
DEBUG時,程式從第一個斷點處進入暫停狀態,然後根據使用者的指令,一步一步地進行執行,每執行一步,都能夠從控制檯中檢視到程式和資料的所有細節;

【主要操作步驟】
在需要中止的地方打斷點;
在IDE中右鍵選擇“Debug XXX”;
按需【下一步(Step Over)】或【進入方法(Step Into) 】直到流程結束;

在分步執行的過程中,可以:
將重點懷疑的變數右擊新增到觀察(Add to watches);
除錯過程中人為修改可疑變數的值(Set Value);
PS:DEBUG是一種效率不高的除錯手段,它應用作程式除錯的輔助手段而非主要手段;

例:
在這裡插入圖片描述

第三節:關於日誌

什麼是日誌

網路裝置、系統及服務程式等,在運作時都會產生一個叫log的事件記錄;每一行日誌都記載著日期、時間、使用者及動作等相關操作的描述。在軟體專案工程中除錯,不應該是DEBUG來進行的,應為檢測的效率不高,應該以日誌+單元測試為主要的方式。而日誌往往都是後來用於回看的,通過回看日誌可以發現當時發生了什麼異常。Web通常把日誌寫到一個檔案中

日誌常用API

【全域性日誌logging】
basicConfig(level,format) —— 基本設定(級別,格式)
getLogger(name) —— 建立區域性日誌物件
Fomatter('%(asctime)s,%(name)s,%(levelname)s,%(message)s') —— 設定日誌格式,name 指區域性日誌名
FileHandler("./logs/log.txt") —— 檔案處理器,把日誌傳向檔案
StreamHandler() —— 流處理器,把日誌傳向控制檯
RotatingFileHandler("./logs/log3.txt",maxBytes=1*1024,backupCount=3) —— 回滾的檔案處理器,傳向動態儲存上限的檔案
logging.config.dictConfig(configDict) —— 字典設定,擴大了基本設定的侷限性

【區域性日誌Logger】
info(msg) —— 列印資訊
debug(msg) —— 列印除錯資訊
waming(msg) —— 列印敬告資訊
error(msg,exc_info=True) —— 列印錯誤資訊
setLevel(level) —— 設定上限等級
addHandler(handler) —— 設定處理器物件

【處理日誌Handler】
setLevel(logging.INFO) —— 設定等級
setFormatter(formatter) —— 設定格式

日誌的等級

Python原始碼對日誌級別的是有定義的,數值越大,級別越高;
輸出時,等於或高於配置級別的日誌資訊都會被輸出;

【不同的級別的具體含義】
FATAL —— 致命錯誤
CRITICAL —— 特別糟糕的事情,如記憶體耗盡、磁碟空間為空,一般很少使用
ERROR —— 發生錯誤時,如IO操作失敗或者連線問題
WARNING —— 發生很重要的事件,但是並不是錯誤時,如使用者登入密碼錯誤
INFO —— 處理請求或者狀態變化等日常事務
DEBUG —— 除錯過程中使用DEBUG等級,如演算法中每個迴圈的中間狀態
NOTSET —— 未設定

例:Python原始碼
在這裡插入圖片描述

日誌的格式

%(levelno)s —— 列印日誌級別的數值
%(levelname)s —— 列印日誌級別的名稱
%(pathname)s —— 列印當前執行程式的路徑
%(filename)s —— 列印當前執行程式名
%(funcName)s —— 列印日誌的當前函式
%(lineno)d —— 列印日誌的當前行號
%(asctime)s —— 列印日誌的時間
%(thread)d —— 列印執行緒ID
%(threadName)s —— 列印執行緒名稱
%(process)d —— 列印程序ID
%(message)s —— 列印日誌資訊

向控制檯輸出日誌

例1:輸出的級別

import logging 

# 基本配置 
# level=logging.WARNING 輸出WARNING以上的級別的日誌內容 
# format定義了日誌輸出格式:'輸出時間 - 日誌名稱 - 日誌級別 - 日誌內容
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 

# 列印日誌 
logging.critical("大家好,我是critical,最高等級的錯誤資訊")
logging.error("大家好,我是error,等級第二的嚴重錯誤資訊")
logging.warning("大家好,warning,中等級的警告")
logging.info("大家好,我是INFO,初等級的提示錯誤")
logging.debug("大家好,我是debug,只是小人物,大家忽略我吧")

執行結果:
在這裡插入圖片描述

例2:區域性日誌

import logging

# 基本配置
#level=logging.DEBUG 輸出DEBUG以上級別的日誌內容
# format定義了日誌輸出格式:'輸出時間 - 日誌名稱 - 日誌級別 - 日誌內容'
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# 獲取logger物件,命名為__name__,也可以命名為其他,如:mylog
logger = logging.getLogger(__name__)

#設定區域性日誌輸出等級(輸出所有等級)
logger.setLevel(logging.NOTSET)  # 完全沒用,受限於根日誌的設定等級

# 列印區域性日誌
logger.critical("大家好,我是critical,最高等級的錯誤資訊")
logger.error("大家好,我是error,等級第二的嚴重錯誤資訊")
logger.warning("大家好,warning,中等級的警告")
logger.info("大家好,我是INFO,初等級的提示錯誤")
logger.debug("大家好,我是debug,只是小人物,大家忽略我吧")

執行結果:
在這裡插入圖片描述

向檔案輸出日誌

向檔案輸出日誌

【步驟】
1、建立logging.getLogger區域性日誌物件logger,設定其等級
2、建立一個logging.FileHandler物件handler處理器,設定其等級
3、定義日誌格式,並賦於handler處理器的格式設定
4、區域性日誌logger新增處理器handler
5、列印日誌

例:

import logging

# 獲取logger物件,設定日誌級別
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)

# 獲取檔案處理器,並設定級別
handler = logging.FileHandler("./log.txt")
# handler = logging.FileHandler("./logs/log.csv")
handler.setLevel(logging.INFO)

# 獲取並設定檔案處理器的日誌格式
formatter = logging.Formatter('%(asctime)s,%(name)s,%(levelname)s,%(message)s')
handler.setFormatter(formatter)

# 設定日誌處理器
logger.addHandler(handler)

# 列印日誌
logger.critical("critical:you are lose")
logger.error("error:you make a big error")
logger.warning("warning:something is warning")
logger.info("INFO:mistake is coming")
logger.debug("debug:just a debug")

執行結果:
在這裡插入圖片描述

同時向控制檯和檔案輸出日誌

【步驟】
1、建立logging.getLogger區域性日誌物件logger,設定其等級
2、建立一個logging.FileHandler物件handler處理器,設定其等級
3、定義日誌格式,並賦於handler處理器的格式設定
4、建立一個logging.StreamHandler物件console流處理器,設定其等級
5、用剛才定義的日誌格式,並賦於console流處理器的格式設定
6、區域性日誌logger新增處理器handler和流處理器console
7、列印日誌

例:

import logging

# 建立logging.getLogger區域性日誌物件logger,設定其等級
logger = logging.getLogger("mylog")
logger.setLevel(level=logging.INFO)

# 建立一個logging.FileHandler物件handler處理器,設定其等級
handler = logging.FileHandler("./log2.txt")
handler.setLevel(logging.INFO)

# 定義日誌格式,並賦於handler處理器的格式設定
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# 建立一個logging.StreamHandler物件console流處理器,設定其等級
console = logging.StreamHandler()
console.setLevel(logging.INFO)

# 區域性日誌logger新增處理器handler和流處理器console
logger.addHandler(handler)
logger.addHandler(console)

#用剛才定義的日誌格式,並賦於console流處理器的格式設定(略)
pass

# 列印日誌
logger.critical("critical:you are lose")
logger.error("error:you make a big error")
logger.warning("warning:something is warning")
logger.info("INFO:mistake is coming")
logger.debug("debug:just a debug")

執行結果:
在這裡插入圖片描述