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阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Startup 单元测试的核心价值在于两点: 更加精确地定义某段代码的作用,从而使代码的耦合性更低 避免程序员写出...
    wuwenxiang阅读 10,081评论 1 27
  • 官方文档 : https://docs.python.org/dev/library/unittest.mock....
    PPMac阅读 1,697评论 0 3
  • Android单元测试介绍 处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单...
    东经315度阅读 3,089评论 6 37
  • 本文试图总结编写单元测试的流程,以及自己在写单元测试时踩到的一些坑。如有遗漏,纯属必然,欢迎补充。 目录概览: 编...
    苏尚君阅读 3,416评论 0 4
  • 世界杯赛场历来是几家欢喜几家忧,有人欢笑就有人流泪。含冤出局的委屈、有苦说不出的愤懑和无力回天的绝望,共同构成了世...
    点球魅阅读 414评论 0 0