單元測試
什么是單元
單元測試(unit testing),是指對軟件中的最小可測試單元(一個模塊、一個函數或者一個類)進行檢查和驗證。
示例
比如對函數abs(),我們可以編寫出以下幾個測試用例:
輸入正數,比如1、1.2、0.99,期待返回值與輸入相同;
輸入負數,比如-1、-1.2、-0.99,期待返回值與輸入相反;
輸入0,期待返回0;
輸入非數值類型,比如None、[]、{},期待拋出TypeError。
把上面的測試用例放到一個測試模塊里,就是一個完整的單元測試。
做什么
如果單元測試通過,說明我們測試的這個函數能夠正常工作。如果單元測試不通過,要么函數有bug,要么測試條件輸入不正確,總之,需要修復使單元測試能夠通過。
意義
如果我們對abs()函數代碼做了修改,只需要再跑一遍單元測試,如果通過,說明我們的修改不會對abs()函數原有的行為造成影響,如果測試不通過,說明我們的修改與原有行為不一致,要么修改代碼,要么修改測試。
這種以測試為驅動的開發模式最大的好處就是確保一個程序模塊的行為符合我們設計的測試用例。在將來修改的時候,可以極大程度地保證該模塊行為仍然是正確的。
編寫Python單元測試
unittest官方文檔:https://docs.python.org/2/library/unittest.html#assert-methods
unittest庫使用示例
import unittest
class TestStringMethods(unittest.TestCase):
#每個測試類繼承于unittest.TestCase類
def setUp(self):
print 'setUp...'
#每個testXXX函數運行前會先運行setUp函數
def tearDown(self):
print 'tearDown...'
#每個testXXX函數運行后會運行tearDown函數
#每個測試函數必須以test開頭,否則不會被當成測試函數
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執行測試
if __name__ == '__main__':
unittest.main()
setUp()和tearDown()方法
這兩個方法會分別在每調用一個測試方法的前后分別被執行。設想你的測試需要啟動一個數據庫,這時,就可以在setUp()方法中連接數據庫,在tearDown()方法中關閉數據庫,這樣,不必在每個測試方法中重復相同的代碼:
class TestDict(unittest.TestCase):
def setUp(self):
print 'setUp...'
def tearDown(self):
print 'tearDown...'
unitest.skip裝飾器
可以使用unitest.skip裝飾器族跳過test method或者test class,這些裝飾器包括:
① @unittest.skip(reason):無條件跳過測試,reason描述為什么跳過測試
② @unittest.skipif(conditition,reason):condititon為true時跳過測試: 這里完全可以應用條件去控制用例是否執行了,很靈活
③ @unittest.skipunless(condition,reason):condition不是true時跳過測試
unittest中的assertXXX方法用來驗證輸入與輸出是否一致,常用的方法如下:
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:斷言發生的exception
callable:被調用的模塊
*args, **kwds:參數
如果發生的異常與exception一樣,測試通過.
運行單元測試
- 在單元測試類所在的py文件(假設為test.py)最后添加以下語句:
if __name__ == '__main__':
unittest.main()
運行:
$ python test.py
- 另一種更常見的方法是在命令行通過參數-m unittest直接運行單元測試:
$ python -m unittest test
Mock
Mock類庫是一個專門用于在unittest過程中制作(偽造)和修改(篡改)測試對象的類庫,制作和修改的目的是避免這些對象在單元測試過程中依賴外部資源(網絡資源,數據庫連接,其它服務以及耗時過長等)
官方文檔https://docs.python.org/dev/library/unittest.mock.html
安裝
Python 2.7中沒有集成mock庫,Python3中的unittest集成了mock庫
Python 2.7環境下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',否則報錯
示例
#module.py
class Count():
def add(self, a, b):
return a + b
測試用例:
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參數和return_value是相反的。它給mock分配了可替換的結果,覆蓋了return_value。簡單的說,一個模擬工廠調用將返回side_effect值,而不是return_value。
所以,設置side_effect參數為Count類add()方法,那么return_value的作用失效。
測試依賴
例如,我們要測試A模塊,然后A模塊依賴于B模塊的調用。但是,由于B模塊的改變,導致了A模塊返回結果的改變,從而使A模塊的測試用例失敗。其實,對于A模塊,以及A模塊的用例來說,并沒有變化,不應該失敗才對。
通過mock模擬掉影響A模塊的部分(B模塊)。至于mock掉的部分(B模塊)應該由其它用例來測試。
# 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
然后,針對 add_and_multiply()函數編寫測試用例。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()函數依賴了multiply()函數的返回值。如果這個時候修改multiply()函數的代碼。
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)
測試用例運行失敗了,然而,add_and_multiply()函數以及它的測試用例并沒有做任何修改,罪魁禍首是multiply()函數引起的,我們應該把 multiply()函數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()裝飾/上下文管理器可以很容易地模擬類或對象在模塊測試。在測試過程中,您指定的對象將被替換為一個模擬(或其他對象),并在測試結束時還原。
這里模擬function.py文件中multiply()函數。
def test_add_and_multiply2(self, mock_multiply):
在定義測試用例中,將mock的multiply()函數(對象)重命名為 mock_multiply對象。
mock_multiply.return_value = 15
設定mock_multiply對象的返回值為固定的15。
ock_multiply.assert_called_once_with(3, 5)
檢查ock_multiply方法的參數是否正確。
tox使用
官方文檔:http://tox.readthedocs.io/en/latest/example/basic.html
參考文檔:http://www.tuicool.com/articles/UnQbyyv
tox是什么
tox是通用的虛擬環境管理和測試命令行工具。
tox作用
- 用不同的Python版本和解釋器檢查你的軟件包是否正確安裝
- 在不同的虛擬環境中運行測試,配置你選擇的測試工具
- 作為持續集成服務器的前端,大大減少了樣板和合并CI和基于shell的測試
基礎示例
安裝:
$ pip install tox
在tox.ini文件中配置你的項目的基本信息和你想要的測試環境.
你還可以通過運行tox-quickstart來自動生成一個tox.ini文件。
要根據Python2.6和Python2.7來安裝和測試您的項目,只需鍵入:
tox
這將打包源碼(sdist-package)到您當前的項目,創建兩個virtualenv環境,將sdist-package安裝到環境中,并在其中運行指定的命令
tox -e py26
詳細配置示例:
[tox]
minversion = 1.6
#最低tox版本
skipsdist = True
#跳過本地軟件包安裝到virtualenv中步驟
envlist = py27,pep8,com
# envlist 表示 tox 中配置的環境都有哪些
[testenv]
# testenv 是默認配置,如果某個環境自身的 section 中沒有定義這些配置, 那么就從這個 section 中讀取
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
PYCURL_SSL_LIBRARY=openssl
# setenv 列出了虛擬機環境中生效的環境變量,一些配色方案和單元測試標志
usedevelop = True
# usedevelop 表示安裝 virtualenv 時, 項目自身是采用開發模式安裝的, 所以不會拷貝代碼到 virtualenv 目錄中, 只是做個鏈接
install_command = pip install {opts} {packages}
# 表示構建環境的時候要執行的命令,一般是使用 pip 安裝
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
# deps 指定構建環境時需要安裝的第三方依賴包
# 每個虛擬環境創建的時候, 會通過 pip install -r requirements.txt 和 pip install -r test-requirements.txt 安裝依賴包到虛擬環境
# 一般的項目會直接安裝 requirements 和 test-requirements 兩個文件中的所有依賴包
commands = ostestr {posargs}
# commands 表示構建好 virtualenv 之后要執行的命令
# 這里調用了 ostestr 指令來調用 testrepository 執行單元測試用例
# {posargs} 參數就是可以將 tox 指令的參數傳遞給 ostestr
whitelist_externals = bash
passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
[testenv:py34]
commands =
python -m testtools.run
# 這個 section 是為 py34 環境定制某些配置的,沒有定制的配置,將會從 [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
# 執行 tox -e pep8 進行代碼檢查, 實際上是執行了上述指令來進行代碼的語法規范檢查
[tox:jenkins]
downloadcache = ~/cache/pip
# 定義了 CI server jenkins 的集成配置
# 指定了 pip 的下載 cache 目錄,提高構建虛擬環境的速度
[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).*$'
# 定義一個 cover 虛擬環境,使單元測試的時候,自動應用 coverage
...
其他常用配置:
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
#設置環境變量
usedevelop = True
#項目應該使用setup.py開發安裝到環境中,而不是使用setup.py install來構建和安裝其源代碼。
依賴requirements.txt文件
將requirements.txt文件添加到deps的三種方式:
deps = -r requirements.txt
deps = -c constraints.txt
deps = -r requirements.txt -c constraints.txt
進行測試
所有的令都是在{toxinidir}(tox.ini所在的目錄)作為當前工作目錄執行的。
在當前目錄執行:
$ tox [-e py27] [subpath]
subpath以Python模塊形式用"."一級一級連接