Python单元测试(unittest+mock+tox)

作者: PPMac | 来源:发表于2017-08-21 17:57 被阅读487次

    单元测试

    什么是单元

    单元测试(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模块形式用"."一级一级连接

    相关文章

      网友评论

        本文标题:Python单元测试(unittest+mock+tox)

        本文链接:https://www.haomeiwen.com/subject/qzqcdxtx.html