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

單元測試

什么是單元

單元測試(unit testing),是指對軟件中的最小可測試單元(一個模塊、一個函數或者一個類)進行檢查和驗證。

test.jpg
示例

比如對函數abs(),我們可以編寫出以下幾個測試用例:

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

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

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

  4. 輸入非數值類型,比如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一樣,測試通過.

運行單元測試
  1. 在單元測試類所在的py文件(假設為test.py)最后添加以下語句:
if __name__ == '__main__':
    unittest.main()

運行:

$ python test.py
  1. 另一種更常見的方法是在命令行通過參數-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模塊形式用"."一級一級連接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容

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