第九章:除錯與測試
文章目錄
第一節:斷言與單元測試
斷言
格式:
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)
'''
文件測試指令碼
#預測加法的結果
>>> 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")
執行結果: