Python單元測(cè)試(unittest+mock+tox)

單元測(cè)試

什么是單元

單元測(cè)試(unit testing),是指對(duì)軟件中的最小可測(cè)試單元(一個(gè)模塊、一個(gè)函數(shù)或者一個(gè)類)進(jìn)行檢查和驗(yàn)證。

test.jpg
示例

比如對(duì)函數(shù)abs(),我們可以編寫出以下幾個(gè)測(cè)試用例:

  1. 輸入正數(shù),比如1、1.2、0.99,期待返回值與輸入相同;

  2. 輸入負(fù)數(shù),比如-1、-1.2、-0.99,期待返回值與輸入相反;

  3. 輸入0,期待返回0;

  4. 輸入非數(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è)試
  1. 在單元測(cè)試類所在的py文件(假設(shè)為test.py)最后添加以下語(yǔ)句:
if __name__ == '__main__':
    unittest.main()

運(yùn)行:

$ python test.py
  1. 另一種更常見的方法是在命令行通過(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í)連接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,179評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,444評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,948評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,185評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,717評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,794評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,414評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,750評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容

  • Startup 單元測(cè)試的核心價(jià)值在于兩點(diǎn): 更加精確地定義某段代碼的作用,從而使代碼的耦合性更低 避免程序員寫出...
    wuwenxiang閱讀 10,125評(píng)論 1 27
  • 官方文檔 : https://docs.python.org/dev/library/unittest.mock....
    PPMac閱讀 1,709評(píng)論 0 3
  • Android單元測(cè)試介紹 處于高速迭代開發(fā)中的Android項(xiàng)目往往需要除黑盒測(cè)試外更加可靠的質(zhì)量保障,這正是單...
    東經(jīng)315度閱讀 3,139評(píng)論 6 37
  • 本文試圖總結(jié)編寫單元測(cè)試的流程,以及自己在寫單元測(cè)試時(shí)踩到的一些坑。如有遺漏,純屬必然,歡迎補(bǔ)充。 目錄概覽: 編...
    蘇尚君閱讀 3,432評(píng)論 0 4
  • 世界杯賽場(chǎng)歷來(lái)是幾家歡喜幾家憂,有人歡笑就有人流淚。含冤出局的委屈、有苦說(shuō)不出的憤懣和無(wú)力回天的絕望,共同構(gòu)成了世...
    點(diǎn)球魅閱讀 440評(píng)論 0 0