單元測(cè)試
什么是單元
單元測(cè)試(unit testing),是指對(duì)軟件中的最小可測(cè)試單元(一個(gè)模塊、一個(gè)函數(shù)或者一個(gè)類)進(jìn)行檢查和驗(yàn)證。
示例
比如對(duì)函數(shù)abs(),我們可以編寫出以下幾個(gè)測(cè)試用例:
輸入正數(shù),比如1、1.2、0.99,期待返回值與輸入相同;
輸入負(fù)數(shù),比如-1、-1.2、-0.99,期待返回值與輸入相反;
輸入0,期待返回0;
輸入非數(shù)值類型,比如None、[]、{},期待拋出TypeError。
把上面的測(cè)試用例放到一個(gè)測(cè)試模塊里,就是一個(gè)完整的單元測(cè)試。
做什么
如果單元測(cè)試通過(guò),說(shuō)明我們測(cè)試的這個(gè)函數(shù)能夠正常工作。如果單元測(cè)試不通過(guò),要么函數(shù)有bug,要么測(cè)試條件輸入不正確,總之,需要修復(fù)使單元測(cè)試能夠通過(guò)。
意義
如果我們對(duì)abs()函數(shù)代碼做了修改,只需要再跑一遍單元測(cè)試,如果通過(guò),說(shuō)明我們的修改不會(huì)對(duì)abs()函數(shù)原有的行為造成影響,如果測(cè)試不通過(guò),說(shuō)明我們的修改與原有行為不一致,要么修改代碼,要么修改測(cè)試。
這種以測(cè)試為驅(qū)動(dòng)的開發(fā)模式最大的好處就是確保一個(gè)程序模塊的行為符合我們?cè)O(shè)計(jì)的測(cè)試用例。在將來(lái)修改的時(shí)候,可以極大程度地保證該模塊行為仍然是正確的。
編寫Python單元測(cè)試
unittest官方文檔:https://docs.python.org/2/library/unittest.html#assert-methods
unittest庫(kù)使用示例
import unittest
class TestStringMethods(unittest.TestCase):
#每個(gè)測(cè)試類繼承于unittest.TestCase類
def setUp(self):
print 'setUp...'
#每個(gè)testXXX函數(shù)運(yùn)行前會(huì)先運(yùn)行setUp函數(shù)
def tearDown(self):
print 'tearDown...'
#每個(gè)testXXX函數(shù)運(yùn)行后會(huì)運(yùn)行tearDown函數(shù)
#每個(gè)測(cè)試函數(shù)必須以test開頭,否則不會(huì)被當(dāng)成測(cè)試函數(shù)
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
#使本py文件可以直接$ python test.py執(zhí)行測(cè)試
if __name__ == '__main__':
unittest.main()
setUp()和tearDown()方法
這兩個(gè)方法會(huì)分別在每調(diào)用一個(gè)測(cè)試方法的前后分別被執(zhí)行。設(shè)想你的測(cè)試需要啟動(dòng)一個(gè)數(shù)據(jù)庫(kù),這時(shí),就可以在setUp()方法中連接數(shù)據(jù)庫(kù),在tearDown()方法中關(guān)閉數(shù)據(jù)庫(kù),這樣,不必在每個(gè)測(cè)試方法中重復(fù)相同的代碼:
class TestDict(unittest.TestCase):
def setUp(self):
print 'setUp...'
def tearDown(self):
print 'tearDown...'
unitest.skip裝飾器
可以使用unitest.skip裝飾器族跳過(guò)test method或者test class,這些裝飾器包括:
① @unittest.skip(reason):無(wú)條件跳過(guò)測(cè)試,reason描述為什么跳過(guò)測(cè)試
② @unittest.skipif(conditition,reason):condititon為true時(shí)跳過(guò)測(cè)試: 這里完全可以應(yīng)用條件去控制用例是否執(zhí)行了,很靈活
③ @unittest.skipunless(condition,reason):condition不是true時(shí)跳過(guò)測(cè)試
unittest中的assertXXX方法用來(lái)驗(yàn)證輸入與輸出是否一致,常用的方法如下:
Method | Checks that | New in |
---|---|---|
assertEqual(a, b) | a == b | |
assertNotEqual(a, b) | a != b | |
assertTrue(x) | bool(x) is True | |
assertFalse(x) | bool(x) is False | |
assertIs(a, b) | a is b | 2.7 |
assertIsNot(a, b) | a is not b | 2.7 |
assertIsNone(x) | x is None | 2.7 |
assertIsNotNone(x) | x is not None | 2.7 |
assertIn(a, b) | a in b | 2.7 |
assertNotIn(a, b) | a not in b | 2.7 |
assertIsInstance(a, b) | isinstance(a, b) | 2.7 |
assertNotIsInstance(a, b) | not isinstance(a, b) | 2.7 |
Method | Used to compare | New in |
---|---|---|
assertMultiLineEqual(a, b) | strings | 2.7 |
assertSequenceEqual(a, b) | sequences | 2.7 |
assertListEqual(a, b) | lists | 2.7 |
assertTupleEqual(a, b) | tuples | 2.7 |
assertSetEqual(a, b) | sets or frozensets | 2.7 |
assertDictEqual(a, b) | dicts | 2.7 |
異常斷言
assertRaises(exception, callable, *args, **kwds)
exception:斷言發(fā)生的exception
callable:被調(diào)用的模塊
*args, **kwds:參數(shù)
如果發(fā)生的異常與exception一樣,測(cè)試通過(guò).
運(yùn)行單元測(cè)試
- 在單元測(cè)試類所在的py文件(假設(shè)為test.py)最后添加以下語(yǔ)句:
if __name__ == '__main__':
unittest.main()
運(yùn)行:
$ python test.py
- 另一種更常見的方法是在命令行通過(guò)參數(shù)-m unittest直接運(yùn)行單元測(cè)試:
$ python -m unittest test
Mock
Mock類庫(kù)是一個(gè)專門用于在unittest過(guò)程中制作(偽造)和修改(篡改)測(cè)試對(duì)象的類庫(kù),制作和修改的目的是避免這些對(duì)象在單元測(cè)試過(guò)程中依賴外部資源(網(wǎng)絡(luò)資源,數(shù)據(jù)庫(kù)連接,其它服務(wù)以及耗時(shí)過(guò)長(zhǎng)等)
官方文檔https://docs.python.org/dev/library/unittest.mock.html
安裝
Python 2.7中沒(méi)有集成mock庫(kù),Python3中的unittest集成了mock庫(kù)
Python 2.7環(huán)境下pip安裝:
$ pip install mock
快速使用
>>> from mock import MagicMock #MagicMock為Mock的子類
>>> thing = ProductionClass()
>>> thing.method = MagicMock(return_value=3)
#指定返回3
>>> thing.method(3, 4, 5, key='value')
3
>>> thing.method.assert_called_with(3, 4, 5, key='value')
#斷言輸入是否為3,4,5,key='value',否則報(bào)錯(cuò)
示例
#module.py
class Count():
def add(self, a, b):
return a + b
測(cè)試用例:
from unittest import mock
import unittest
from module import Count
class MockDemo(unittest.TestCase):
def test_add(self):
count = Count()
count.add = mock.Mock(return_value=13, side_effect=count.add)
result = count.add(8, 8)
print(result)
count.add.assert_called_with(8, 8)
self.assertEqual(result, 16)
if __name__ == '__main__':
unittest.main()
count.add = mock.Mock(return_value=13, side_effect=count.add)
side_effect參數(shù)和return_value是相反的。它給mock分配了可替換的結(jié)果,覆蓋了return_value。簡(jiǎn)單的說(shuō),一個(gè)模擬工廠調(diào)用將返回side_effect值,而不是return_value。
所以,設(shè)置side_effect參數(shù)為Count類add()方法,那么return_value的作用失效。
測(cè)試依賴
例如,我們要測(cè)試A模塊,然后A模塊依賴于B模塊的調(diào)用。但是,由于B模塊的改變,導(dǎo)致了A模塊返回結(jié)果的改變,從而使A模塊的測(cè)試用例失敗。其實(shí),對(duì)于A模塊,以及A模塊的用例來(lái)說(shuō),并沒(méi)有變化,不應(yīng)該失敗才對(duì)。
通過(guò)mock模擬掉影響A模塊的部分(B模塊)。至于mock掉的部分(B模塊)應(yīng)該由其它用例來(lái)測(cè)試。
# function.py
def add_and_multiply(x, y):
addition = x + y
multiple = multiply(x, y)
return (addition, multiple)
def multiply(x, y):
return x * y
然后,針對(duì) add_and_multiply()函數(shù)編寫測(cè)試用例。func_test.py
import unittest
import function
class MyTestCase(unittest.TestCase):
def test_add_and_multiply(self):
x = 3
y = 5
addition, multiple = function.add_and_multiply(x, y)
self.assertEqual(8, addition)
self.assertEqual(15, multiple)
if __name__ == "__main__":
unittest.main()
add_and_multiply()函數(shù)依賴了multiply()函數(shù)的返回值。如果這個(gè)時(shí)候修改multiply()函數(shù)的代碼。
def multiply(x, y):
return x * y + 3
python3 func_test.py
F
======================================================================
FAIL: test_add_and_multiply (main.MyTestCase)
Traceback (most recent call last):
File "fun_test.py", line 19, in test_add_and_multiply
self.assertEqual(15, multiple)
AssertionError: 15 != 18
Ran 1 test in 0.000s
FAILED (failures=1)
測(cè)試用例運(yùn)行失敗了,然而,add_and_multiply()函數(shù)以及它的測(cè)試用例并沒(méi)有做任何修改,罪魁禍?zhǔn)资莔ultiply()函數(shù)引起的,我們應(yīng)該把 multiply()函數(shù)mock掉。
import unittest
from unittest.mock import patch
import function
class MyTestCase(unittest.TestCase):
@patch("function.multiply")
def test_add_and_multiply2(self, mock_multiply):
x = 3
y = 5
mock_multiply.return_value = 15
addition, multiple = function.add_and_multiply(x, y)
mock_multiply.assert_called_once_with(3, 5)
self.assertEqual(8, addition)
self.assertEqual(15, multiple)
if __name__ == "__main__":
unittest.main()
@patch("function.multiply")
patch()裝飾/上下文管理器可以很容易地模擬類或?qū)ο笤谀K測(cè)試。在測(cè)試過(guò)程中,您指定的對(duì)象將被替換為一個(gè)模擬(或其他對(duì)象),并在測(cè)試結(jié)束時(shí)還原。
這里模擬function.py文件中multiply()函數(shù)。
def test_add_and_multiply2(self, mock_multiply):
在定義測(cè)試用例中,將mock的multiply()函數(shù)(對(duì)象)重命名為 mock_multiply對(duì)象。
mock_multiply.return_value = 15
設(shè)定mock_multiply對(duì)象的返回值為固定的15。
ock_multiply.assert_called_once_with(3, 5)
檢查ock_multiply方法的參數(shù)是否正確。
tox使用
官方文檔:http://tox.readthedocs.io/en/latest/example/basic.html
參考文檔:http://www.tuicool.com/articles/UnQbyyv
tox是什么
tox是通用的虛擬環(huán)境管理和測(cè)試命令行工具。
tox作用
- 用不同的Python版本和解釋器檢查你的軟件包是否正確安裝
- 在不同的虛擬環(huán)境中運(yùn)行測(cè)試,配置你選擇的測(cè)試工具
- 作為持續(xù)集成服務(wù)器的前端,大大減少了樣板和合并CI和基于shell的測(cè)試
基礎(chǔ)示例
安裝:
$ pip install tox
在tox.ini文件中配置你的項(xiàng)目的基本信息和你想要的測(cè)試環(huán)境.
你還可以通過(guò)運(yùn)行tox-quickstart來(lái)自動(dòng)生成一個(gè)tox.ini文件。
要根據(jù)Python2.6和Python2.7來(lái)安裝和測(cè)試您的項(xiàng)目,只需鍵入:
tox
這將打包源碼(sdist-package)到您當(dāng)前的項(xiàng)目,創(chuàng)建兩個(gè)virtualenv環(huán)境,將sdist-package安裝到環(huán)境中,并在其中運(yùn)行指定的命令
tox -e py26
詳細(xì)配置示例:
[tox]
minversion = 1.6
#最低tox版本
skipsdist = True
#跳過(guò)本地軟件包安裝到virtualenv中步驟
envlist = py27,pep8,com
# envlist 表示 tox 中配置的環(huán)境都有哪些
[testenv]
# testenv 是默認(rèn)配置,如果某個(gè)環(huán)境自身的 section 中沒(méi)有定義這些配置, 那么就從這個(gè) section 中讀取
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
PYCURL_SSL_LIBRARY=openssl
# setenv 列出了虛擬機(jī)環(huán)境中生效的環(huán)境變量,一些配色方案和單元測(cè)試標(biāo)志
usedevelop = True
# usedevelop 表示安裝 virtualenv 時(shí), 項(xiàng)目自身是采用開發(fā)模式安裝的, 所以不會(huì)拷貝代碼到 virtualenv 目錄中, 只是做個(gè)鏈接
install_command = pip install {opts} {packages}
# 表示構(gòu)建環(huán)境的時(shí)候要執(zhí)行的命令,一般是使用 pip 安裝
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
# deps 指定構(gòu)建環(huán)境時(shí)需要安裝的第三方依賴包
# 每個(gè)虛擬環(huán)境創(chuàng)建的時(shí)候, 會(huì)通過(guò) pip install -r requirements.txt 和 pip install -r test-requirements.txt 安裝依賴包到虛擬環(huán)境
# 一般的項(xiàng)目會(huì)直接安裝 requirements 和 test-requirements 兩個(gè)文件中的所有依賴包
commands = ostestr {posargs}
# commands 表示構(gòu)建好 virtualenv 之后要執(zhí)行的命令
# 這里調(diào)用了 ostestr 指令來(lái)調(diào)用 testrepository 執(zhí)行單元測(cè)試用例
# {posargs} 參數(shù)就是可以將 tox 指令的參數(shù)傳遞給 ostestr
whitelist_externals = bash
passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
[testenv:py34]
commands =
python -m testtools.run
# 這個(gè) section 是為 py34 環(huán)境定制某些配置的,沒(méi)有定制的配置,將會(huì)從 [testenv] 讀取
[testenv:pep8]
commands =
flake8 {posargs} ./egis egis/common
# Check that .po and .pot files are valid:
bash -c "find egis -type f -regex '.*\.pot?' -print0|xargs -0 -n 1 msgfmt --check-format -o /dev/null"
{toxinidir}/tools/config/check_uptodate.sh
{toxinidir}/tools/check_exec.py {toxinidir}/egis
# 執(zhí)行 tox -e pep8 進(jìn)行代碼檢查, 實(shí)際上是執(zhí)行了上述指令來(lái)進(jìn)行代碼的語(yǔ)法規(guī)范檢查
[tox:jenkins]
downloadcache = ~/cache/pip
# 定義了 CI server jenkins 的集成配置
# 指定了 pip 的下載 cache 目錄,提高構(gòu)建虛擬環(huán)境的速度
[testenv:cover]
# Also do not run test_coverage_ext tests while gathering coverage as those
# tests conflict with coverage.
commands =
python setup.py testr --coverage \
--testr-args='^(?!.*test.*coverage).*$'
# 定義一個(gè) cover 虛擬環(huán)境,使單元測(cè)試的時(shí)候,自動(dòng)應(yīng)用 coverage
...
其他常用配置:
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
#設(shè)置環(huán)境變量
usedevelop = True
#項(xiàng)目應(yīng)該使用setup.py開發(fā)安裝到環(huán)境中,而不是使用setup.py install來(lái)構(gòu)建和安裝其源代碼。
依賴requirements.txt文件
將requirements.txt文件添加到deps的三種方式:
deps = -r requirements.txt
deps = -c constraints.txt
deps = -r requirements.txt -c constraints.txt
進(jìn)行測(cè)試
所有的令都是在{toxinidir}(tox.ini所在的目錄)作為當(dāng)前工作目錄執(zhí)行的。
在當(dāng)前目錄執(zhí)行:
$ tox [-e py27] [subpath]
subpath以Python模塊形式用"."一級(jí)一級(jí)連接