Python 單元測(cè)試(unittest)的使用小結(jié)
測(cè)試目錄
項(xiàng)目的整體結(jié)構(gòu)可以參考“軟件目錄開(kāi)發(fā)規(guī)范”,這里單說(shuō)測(cè)試目錄。一般都是在項(xiàng)目里單獨(dú)創(chuàng)建一個(gè)測(cè)試目錄,目錄名就是“tests”。
關(guān)于目錄的位置,一種建議是,在項(xiàng)目名(假設(shè)項(xiàng)目名是Foo)的一級(jí)子目錄下創(chuàng)建二級(jí)子目錄 “Foo/foo/tests” 。但是這樣可能是因?yàn)橛闷饋?lái)不方便,有很多是按下面的做法。不過(guò)下面的示例我還是用這個(gè)方法來(lái)創(chuàng)建測(cè)試目錄。
還可以把測(cè)試目錄向上移一層,作為一級(jí)子目錄,直接創(chuàng)建在項(xiàng)目之下 “Foo/tests”。參考django、scrapy、flask都是這樣的做法。
測(cè)試函數(shù)
標(biāo)題的意思是對(duì)函數(shù)(def)進(jìn)行測(cè)試,相對(duì)于測(cè)試類(class)。
學(xué)習(xí)測(cè)試,得有要測(cè)試的代碼。下面是一個(gè)簡(jiǎn)單的函數(shù),接收城市名和國(guó)家名,返回一個(gè)格式為“City, Country“這樣的字符串:
# UnitTest/unit_test/utils/city_functions.py def get_city_info(city, country): city_info = "%s, %s" % (city, country) return city_info.title()
接下來(lái)就對(duì)上面的這個(gè)函數(shù)進(jìn)行測(cè)試。
手動(dòng)測(cè)試
現(xiàn)在來(lái)寫一個(gè)使用這個(gè)函數(shù)的程序:
# UnitTest/unit_test/test/cities.py try: from unit_test.utils.city_functions import get_city_info except ModuleNotFoundError: import sys sys.path.append('../..') from unit_test.utils.city_functions import get_city_info print("Enter 'q' at any time to quit.") while True: city = input("city name: ") if city == 'q': break country = input("country name: ") if country == 'q': break fullname = get_city_info(city, country) print("\tcity info:", fullname)
然后運(yùn)行的結(jié)果:
Enter 'q' at any time to quit.
city name: shanghai
country name: china
city info: Shanghai, China
city name: qProcess finished with exit code 0
上面這樣是手動(dòng)測(cè)試,還是得有一種自動(dòng)測(cè)試函數(shù)輸出的高效方式。如果能夠?qū)et_fullname()進(jìn)行自動(dòng)測(cè)試,就能始終確信,給這個(gè)函數(shù)提供測(cè)試過(guò)的姓名后,它能返回正確的結(jié)果。尤其是在對(duì)函數(shù)進(jìn)行修改的前后。
模塊導(dǎo)入路徑的問(wèn)題
PyCharm會(huì)自動(dòng)把項(xiàng)目目錄加到環(huán)境變量里去,在PyCharm里執(zhí)行都沒(méi)問(wèn)題。但是如果不用PyCharm而是單獨(dú)運(yùn)行,這個(gè)目錄結(jié)構(gòu)應(yīng)該會(huì)有點(diǎn)問(wèn)題,會(huì)找不到需要測(cè)試的函數(shù)。簡(jiǎn)單點(diǎn)就是把測(cè)試用例和被測(cè)試的函數(shù)放到同一個(gè)目錄里,然后改一下 from import 就可以正常運(yùn)行了。或者自己手動(dòng)添加環(huán)境變量,就像例子里那樣。
單元測(cè)試-unittest
Python標(biāo)準(zhǔn)庫(kù)中的模塊unittest提供了代碼測(cè)試工具。
創(chuàng)建測(cè)試用例
為函數(shù)編寫測(cè)試用例,可先導(dǎo)入模塊unittest以及要測(cè)試的函數(shù),再創(chuàng)建一個(gè)繼承unittest.TestCase的類,并編寫一系列方法對(duì)函數(shù)行為的不同方面進(jìn)行測(cè)試。
下面是一個(gè)只包含一個(gè)方法的測(cè)試用例:
# UnitTest/unit_test/test/test_city_functions.py import unittest try: from unit_test.utils.city_functions import get_city_info except ModuleNotFoundError: import sys sys.path.append('../..') from unit_test.utils.city_functions import get_city_info class CitiesTestCase(unittest.TestCase): """測(cè)試city_functions.py""" def test_city_country(self): city_info = get_city_info('shanghai', 'china') self.assertEqual(city_info, 'Shanghai, China') def test_New_York(self): city_info = get_city_info('new york', 'America') self.assertEqual(city_info, 'New York, America') if __name__ == '__main__': unittest.main()
命名的規(guī)則和建議:
- 類名,可以任意起名,但是最好看起來(lái)和測(cè)試有關(guān)并包含Test字樣。
- 方法名,名字必須以“test_”開(kāi)頭,所有以“test_”開(kāi)頭的方法,都會(huì)自動(dòng)運(yùn)行
在測(cè)試的方法的最后,使用了unittest類最有用的功能之一:一個(gè)斷言方法。來(lái)檢查得到的結(jié)果和我們預(yù)期的結(jié)果是否一致。
輸出的效果
最后一行 unittest.main() 讓Python運(yùn)行這個(gè)文件中的測(cè)試。執(zhí)行程序后得到如下的輸出:
.
----------------------------------------------------------------------
Ran 1 test in 0.000sOK
運(yùn)行測(cè)試用例時(shí),每完成一個(gè)單元測(cè)試,Python都打印一個(gè)字符:
- 測(cè)試通過(guò)時(shí)打印一個(gè)句點(diǎn);
- 測(cè)試引發(fā)錯(cuò)誤時(shí)打印一個(gè)E;
- 測(cè)試導(dǎo)致斷言失敗時(shí)打印一個(gè)F。
這就是你運(yùn)行測(cè)試用例時(shí),在輸出的第一行中看到的句點(diǎn)和字符數(shù)量各不相同的原因。如果測(cè)試用例包含很多單元測(cè)試,需要運(yùn)行很長(zhǎng)時(shí)間,就可通過(guò)觀察這些結(jié)果來(lái)獲悉有多少個(gè)測(cè)試通過(guò)了。
PyCharm對(duì)單元測(cè)試做了自己的優(yōu)化,輸出看不到上面的點(diǎn),而是有更加漂亮的展示方式。
測(cè)試不通過(guò)
現(xiàn)在看下測(cè)試不通過(guò)的效果。這里不修改測(cè)試用例,而是對(duì)get_city_info()函數(shù)做一個(gè)update,現(xiàn)在還要顯示城市的人口數(shù)量:
def get_city_info(city, country, population): city_info = "%s, %s - 人口: %d" % (city, country, population) return city_info.title()
這次再執(zhí)行測(cè)試用例,輸出如下:
E
======================================================================
ERROR: test_city_country (__main__.CitiesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_city_functions.py", line 17, in test_city_country
city_info = get_city_info('shanghai', 'china')
TypeError: get_city_info() missing 1 required positional argument: 'population'----------------------------------------------------------------------
Ran 1 test in 0.001sFAILED (errors=1)
這里看的是E而不是之前的點(diǎn),表示有一個(gè)錯(cuò)誤。
測(cè)試未通過(guò)的處理
這里不要去修改之前的測(cè)試用例。假設(shè)update之前的函數(shù)已經(jīng)在項(xiàng)目?jī)?nèi)使用起來(lái)了,現(xiàn)在測(cè)試不通過(guò),表示之前調(diào)用這個(gè)函數(shù)的代碼都有問(wèn)題。如果不想改項(xiàng)目里其它的代碼,這里先嘗試修改get_city_info()函數(shù),讓它能夠通過(guò)測(cè)試,也可以加上新的功能:
# UnitTest/unit_test/utils/city_functions.py def get_city_info(city, country, population=None): if population: city_info = "%s, %s - 人口: %d" % (city, country, population) else: city_info = "%s, %s" % (city, country) return city_info.title()
現(xiàn)在的各個(gè)版本的update才是兼容舊版本的代碼,這次測(cè)試用例就可以通過(guò)了。
添加新測(cè)試
之前的測(cè)試用例只能驗(yàn)證就的功能,現(xiàn)在添加了新功能,是否沒(méi)問(wèn)題,還得通過(guò)測(cè)試來(lái)進(jìn)行驗(yàn)證:
# UnitTest/unit_test/test/test_city_functions.py class CitiesTestCase(unittest.TestCase): """測(cè)試city_functions.py""" def test_city_country(self): city_info = get_city_info('shanghai', 'china') self.assertEqual(city_info, 'Shanghai, China') def test_New_York_population(self): city_info = get_city_info('new york', 'America', 8537673) self.assertEqual(city_info, 'New York, America - 人口: 8537673')
現(xiàn)在新功能的測(cè)試用例也用了,并且2個(gè)測(cè)試都能通過(guò)。以后如果還需要對(duì)get_city_info()函數(shù)進(jìn)行修改,只要再運(yùn)行測(cè)試就可以知道新的代碼是否會(huì)對(duì)原有的項(xiàng)目有影響。
斷言方法
模塊在unittest.TestCase類中提供了很多斷言方法,之前已經(jīng)用一個(gè)了。下面是6個(gè)常用的斷言方法:
- assertEqual(a, b) : 核實(shí)a == b
- assertNotEqual(a, b) : 核實(shí)a != b
- assertTrue(x) : 核實(shí)x為True
- assertFalse(x) : 核實(shí)x為False
- assertIn(item, list) : 核實(shí)item在list中
- assertNotIn(item, list) : 核實(shí)item不在list中
你只能在繼承unittest.TestCase的類中使用這些方法。
測(cè)試類
前面的內(nèi)容只是對(duì)函數(shù)進(jìn)行測(cè)試。很多時(shí)候都會(huì)用到類,因?yàn)檫€需要能夠證明類也可以正常的運(yùn)行。類的測(cè)試與函數(shù)的測(cè)試相似,其中大部分工作都是測(cè)試類中方法的行為,但存在一些不同之處。
準(zhǔn)備要測(cè)試的類
先編寫一個(gè)類來(lái)進(jìn)行測(cè)試,這個(gè)類里存儲(chǔ)了一個(gè)課程名,以及學(xué)習(xí)該課程的學(xué)員:
# UnitTest/unit_test/course.py class CourseManage(object): def __init__(self, course): self.course = course self.students = [] def show_course(self): print("課程:", self.course) def add_student(self, name): self.students.append(name) def show_students(self): print("所有學(xué)員:") for student in self.students: print('-', student)
為證明CourseManage類工作正常,再編寫一個(gè)使用它的程序:
from unit_test.course import CourseManage course = CourseManage("Python") course.show_course() print("準(zhǔn)備錄入學(xué)員...") print("Enter 'q' at any time to quit.\n") while True: resp = input("Student's Name: ") if resp == 'q': break if resp: course.add_student(resp.title()) print("\n錄入完畢...") course.show_students()
這段程序定義了一門課程,并使用課程名創(chuàng)建了一個(gè)CourseManage對(duì)象。接下來(lái)主要就是調(diào)用對(duì)象的add_student()方法來(lái)錄入學(xué)員名字。輸入完畢后,按q能退出。最后會(huì)打印所有的學(xué)員。
所有的輸入和輸出如下:
課程: Python
準(zhǔn)備錄入學(xué)員...
Enter 'q' at any time to quit.Student's Name: oliver queen
Student's Name: barry allen
Student's Name: kara
Student's Name: sara lance
Student's Name: q錄入完畢...
所有學(xué)員:
- Oliver Queen
- Barry Allen
- Kara
- Sara LanceProcess finished with exit code 0
編寫類的測(cè)試用例
下面來(lái)編寫一個(gè)測(cè)試,對(duì)CourseManage類的行為的一個(gè)方面進(jìn)行驗(yàn)證。如果用戶輸入了某個(gè)學(xué)員的名字,這個(gè)名字可以被存儲(chǔ)在self.students的列表里。所以,需要做的是在學(xué)員被錄入后,使用assertIn()這個(gè)斷言方法:
# UnitTest/unit_test/test/test_course.py import unittest from unit_test.course import CourseManage class TestCourseManage(unittest.TestCase): def test_add_student(self): course = CourseManage("Python") name = 'snart' course.add_student(name.title()) self.assertIn('Snart', course.students) if __name__ == '__main__': unittest.main()
上面的方法只驗(yàn)證了錄入一個(gè)學(xué)員的情況,而大多數(shù)情況下都是有很多學(xué)員的。所以,還要添加一個(gè)方法,驗(yàn)證錄入多個(gè)學(xué)員是否正常:
class TestCourseManage(unittest.TestCase): def test_add_student(self): course = CourseManage("Python") name = 'snart' course.add_student(name.title()) self.assertIn('Snart', course.students) def test_add_students(self): course = CourseManage("Python") name_list = ['oliver queen', 'barry allen', 'kara', 'sara lance'] for name in name_list: course.add_student(name.title()) for name in name_list: self.assertIn(name.title(), course.students)
setUp() 方法
在上面的例子里,每個(gè)測(cè)試方法中都創(chuàng)建了一個(gè)實(shí)例。但是還有一種需求是,我希望只創(chuàng)建一個(gè)實(shí)例,但是要在多個(gè)方法里對(duì)這個(gè)實(shí)例進(jìn)行操作來(lái)反復(fù)驗(yàn)證。在unittest.TestCase類包含方法setUp(),就可以只實(shí)例化一次,并可以在每個(gè)測(cè)試方法中使用。如果在TestCase類中包含了方法setUp(),Python會(huì)先運(yùn)行它,再運(yùn)行各個(gè)以test_打頭的方法。
簡(jiǎn)單點(diǎn)說(shuō),setUp()方法就是在父類里預(yù)留的一個(gè)鉤子,會(huì)在其他測(cè)試方法運(yùn)行前先運(yùn)行:
import unittest from unit_test.course import CourseManage class TestCourseManage(unittest.TestCase): def setUp(self): self.course = CourseManage("Python") self.name_list = ['oliver queen', 'barry allen', 'kara', 'sara lance'] def test_add_student(self): name = 'snart' self.course.add_student(name.title()) self.assertIn('Snart', self.course.students) def test_add_students(self): for name in self.name_list: self.course.add_student(name.title()) for name in self.name_list: self.assertIn(name.title(), self.course.students) if __name__ == '__main__': unittest.main()
測(cè)試自己編寫的類時(shí),使用setUp()方法會(huì)讓測(cè)試方法編寫起來(lái)更容易,下面是建議的做法:
在setUp()方法中創(chuàng)建一系列實(shí)例并設(shè)置它們的屬性,再在測(cè)試方法中直接使用這些實(shí)例。相比于在每個(gè)測(cè)試方法中都創(chuàng)建實(shí)例并設(shè)置其屬性,這要容易得多。
小結(jié)
如果你在項(xiàng)目中包含了初步測(cè)試,其他程序員將更敬佩你,他們將能夠更得心應(yīng)手地嘗試使用你編寫的代碼,也更愿意與你合作開(kāi)發(fā)項(xiàng)目。如果你要跟其他程序員開(kāi)發(fā)的項(xiàng)目共享代碼,就必須證明你編寫的代碼通過(guò)了既有測(cè)試,通常還需要為你添加的新行為編寫測(cè)試。
請(qǐng)通過(guò)多開(kāi)展測(cè)試來(lái)熟悉代碼測(cè)試過(guò)程。對(duì)于自己編寫的函數(shù)和類,請(qǐng)編寫針對(duì)其重要行為的測(cè)試,但在項(xiàng)目早期,不要試圖去編寫全覆蓋的測(cè)試用例,除非有充分的理由這樣做。
pytest
這篇講的是Python內(nèi)置的單元測(cè)試模塊。作為初學(xué)者先用著熟悉起來(lái)就很不錯(cuò)了。
pytest是Python最流程的單測(cè)框架之一。具體可以上GitHub參考下那些開(kāi)源項(xiàng)目的單元測(cè)試,很多用的是這個(gè)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
使用Docker制作Python環(huán)境連接Oracle鏡像
這篇文章主要為大家介紹了使用Docker制作Python環(huán)境連接Oracle鏡像示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06使用PyTorch將文件夾下的圖片分為訓(xùn)練集和驗(yàn)證集實(shí)例
今天小編就為大家分享一篇使用PyTorch將文件夾下的圖片分為訓(xùn)練集和驗(yàn)證集實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-01-01python?與c++相互調(diào)用實(shí)現(xiàn)
這篇文章主要介紹了python?與c++相互調(diào)用實(shí)現(xiàn),我們都知道c++運(yùn)算速度快于python,python又簡(jiǎn)單易寫,很多人就會(huì)想到將兩者結(jié)合,接下倆小編要給大家介紹的就是python?與c++相互調(diào)用實(shí)現(xiàn),,需要的朋友可以參考一下2022-03-03python目標(biāo)檢測(cè)基于opencv實(shí)現(xiàn)目標(biāo)追蹤示例
這篇文章主要為大家介紹了python基于opencv實(shí)現(xiàn)目標(biāo)追蹤示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05Python socket實(shí)現(xiàn)簡(jiǎn)單聊天室
這篇文章主要為大家詳細(xì)介紹了Python socket實(shí)現(xiàn)簡(jiǎn)單聊天室,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04python框架flask入門之環(huán)境搭建及開(kāi)啟調(diào)試
這篇文章主要介紹了python框架flask入門環(huán)境搭建及開(kāi)啟調(diào)試的步驟設(shè)置,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06