Skip to content

Latest commit

 

History

History
1737 lines (1139 loc) · 54.3 KB

代码测试.md

File metadata and controls

1737 lines (1139 loc) · 54.3 KB

代码测试

项目写完后应该写测试以确保代码稳定可靠.经过测试稳定可靠的代码才能经得住时间的考验.但即便有测试,如果代码覆盖不够,测试用例不能覆盖全部情况,这样也并不能保准项目就一定稳健.因此条件允许的话也最好将类型注释写好,利用mypy做好静态类型检验.

将代码分解为独立不耦合的小部件,针对这些小部件的测试就是单元测试.测试单元应该集中于小部分的功能,并且证明在各种情况下它的行为正确(包括错误行为).

常常将测试代码和运行代码一起写是一种非常好的习惯.聪明地使用这种方法将会帮助你更加精确地定义代码的含义,并且代码的耦合性更低.

一般来说,更加推崇的方法是先写测试用例确定代码行为,再写代码,也就是所谓的测试驱动编程.

测试的通用规则

  • 测试单元应该集中于小部分的功能,并且证明它是对的.
  • 每个测试单元应该完全独立.每个都能够单独运行,除了调用的命令,都需在测试套件中.要想实现这个规则,测试单元应该加载最新的数据集,之后再做一些清理.
  • 尽量使测试单元快速运行.如果一个单独的测试单元需要较长的时间去运行,开发进度将会延迟,测试单元将不能如期常态性运行.有时候因为测试单元需要复杂的数据结构,并且当它运行时每次都要加载,所以其运行时间较长.把运行吃力的测试单元放在单独的测试组件中,并且按照需要运行其它测试单元.
  • 学习使用工具,学习如何运行一个单独的测试用例.然后,当在一个模块中开发了一个功能时,经常运行这个功能的测试用例,理想情况下,一切都将自动.
  • 在编码会话前后,要常常运行完整的测试组件.只有这样,你才会坚信剩余的代码不会中断.
  • 实现钩子(hook)是一个非常好的主意.因为一旦把代码放入分享仓库中,这个钩子可以运行所有的测试单元.
  • 如果你在开发期间不得不打断自己的工作,写一个被打断的单元测试,它关于下一步要开发的东西.当回到工作时,你将更快地回到原先被打断的地方,并且步入正轨.
  • 当你调试代码的时候,首先需要写一个精确定位bug的测试单元.尽管这样做很难,但是捕捉bug的单元测试在项目中很重要.
  • 测试函数使用长且描述性的名字.这边的样式指导与运行代码有点不一样,运行代码更倾向于使用短的名字,而测试函数不会直接被调用.在运行代码中,square()或者甚至sqr()这样的命名都是可以的,但是在测试代码中,你应该这样取名test_square_of_number_2(),test_square_negative_number().当测试单元失败时,函数名应该显示,而且尽可能具有描述性.
  • 当发生了一些问题,或者不得不改变时,如果代码中有一套不错的测试单元,维护将很大一部分依靠测试组件解决问题,或者修改确定的行为.因此测试代码应该尽可能多读,甚至多于运行代码.目的不明确的测试单元在这种情况下没有多少用处.
  • 测试代码的另外一个用处是作为新开发人员的入门.当工作基于代码,运行并且阅读相关的测试代码是一个非常好的做法.开发人员将会或者应该发现热点,而这将引起困难和其它情况,如果他们一定要加一些功能,第一步应该是要增加一个测试单元,通过这种方法,确保新功能不再是一个没有被嵌入到接口中的工作路径.

测试的步骤和分类

通常而言我们的测试也是递进式的进行的.对于一个项目而言,我们的的要求从高到底依次提高大致是:

  1. 按预期的输入功能能用不出错
  2. 不按预期输入系统也不容易崩溃(鲁棒性)
  3. 可以更快的处理任务,充分利用资源.

那针对这3个要求,测试可以分为如下几种:

  1. 功能测试(最低限度的保证按预期的输入功能能用不出错)
  2. 单元测试(确保鲁棒性)
  3. 性能测试(用于做性能优化)

本章只讨论前两种测试,第三种测试可以参考本篇的性能调优工具部分

如果是针对多个软件项目或多个模块项目组合实现的复杂项目,那么除了以上三种针对每个子项目的测试外还有如下几种测试用于确保项目整体的质量:

  • 冒烟测试(确保系统的主要功能可以运行)
  • 回归测试(在系统有变化时测试整体是否引入问题)
  • monkey测试(也称搞怪测试,就是用一些随机,稀奇古怪的方式来操作软件,以测试整个系统的健壮性和稳定性)
  • 压力测试(用于测试系统在高负载下的效果,通常要搞到环境崩溃以确定系统能力范围)
  • 灰度测试(这种测试是在生产环境的,是将新版本随机的小范围的发布到线上以获取用户反馈和验证系统稳定性的一种测试)

这一部分会在后面别的篇章中细讲.

本文只介绍在python中如何进行功能测试和单元测试.

本文从的测试例子为如下模块:

%%writefile examples/utest/func_oper_unitest.py
#!/usr/bin/env python

"""\
这里可以写用到多个函数的

"""
from functools import reduce
from operator import mul,add

def multiply(*args):
    """\
    这里可以写单元测试
    >>> multiply(2,3)
    6
    >>> multiply('baka~',3)
    'baka~baka~baka~'
    """
    return reduce(mul,args)

def summing(*args):
    """\
    这里可以写单元测试
    >>> summing(2,3)
    5
    >>> summing(2,3,4)
    9
    """
    return reduce(add,args)
Writing examples/utest/func_oper_unitest.py

功能测试

功能测试作用是最低限度的保证按预期的输入功能能用不出错.其实这种测试也就划定了功能的范围.在python中功能测试可以使用doctest,利用docstring进行,这样即做了功能测试,又相当于在文档中为各个功能提供了例子.

使用doctest利用docstring进行简单测试

Python提供了解析docstring中特定语法内容作为测试代码和待验证结果的工具.

其语法就是:

  • >>> 开头的就是要执行的代码
  • ... 开头的行表示python中的代码段,
  • 要执行代码下面一行就是要验证的内容.

比如a.py:

"""测试乘法接口.

这里可以写单元测试
>>> multiply(2,3)
6
>>> multiply('baka~',3)
'baka~baka~baka~'
"""

而要进行测试有3种方式:

  • 直接使用命令行工具python -m doctest a.py -v

    这种方式只对单文件的模块或脚本有效.

  • 在这个要测试的模块的最后加上

    if __name__ == '__main__':
        import doctest
        doctest.testmod(verbose=True)

    然后直接执行这个文件即可.这种方式只能测试没有复杂依赖关系的模块,否则容易因为执行的文件无法import同级模块报错. 如果要防止这种报错就得修改源码,这就得不偿失了.

  • 在要测的模块外新建一个python脚本,内容为:

    if __name__ == '__main__':
        import doctest
        import mod
        doctest.testmod(mod,verbose=True)

    其中mod为你要测试的包含docstring的模块.这种方式比较推荐放在根模块的__init__.py中,这样就可以直接运行模块以查看功能测试是否通过了.

可以看到doctest写起来一点也不方便,会有除了代码外的很多额外内容需要添加,但它的优点就是代码和结果都可见,因此它的适用场景更多的是作为第一级的子模块的接口的功能测试工具.如果在所有模块中使用doctest那注释的文本量就爆炸了,这样反而更加不利于代码维护.

一个个人比较推荐的模块结构如下:

mod---__init__.py(作为doctest的执行入口)
    |--submod1_1---__init__.py(写doctest)
    |            |--a.py(不写doctest)
    |
    |--submod_2---__init__.py(写doctest)
    |           |--b.py(不写docttest)
    |
    |--c.py(写doctest)

单元测试与unittest模块

单元测试强调覆盖范围,一方面是要求代码要尽可能多的被测试用例覆盖,另一方面也要求接口的输入情况尽可能多的被测试用例覆盖.

python标准库中自带了unittest框架以用来做单元测试.unittest已经可以满足一般的测试需求了.它包含了所有测试框架需要的部件.相比较使用第三方测试框架,unittest相对来说更加稳定可靠,而且作为标准库,也避免了额外的依赖安装.

同时结合第三方包coverage可以很好的输出测试的覆盖率.

使用 unittest 的标准流程为:

  • 从 unittest.TestCase 派生一个子类
  • 在类中定义各种以 "test_" 打头的方法
  • 通过 unittest.main() 函数来启动测试

unittest可以指定测试模块使用

unittest test.py或者unittest test或者unittest test.test

它可以带上以下参数

  • -v 测试的详细信息
  • -f 出错就停
  • -c 可以用ctrol+c停止,并出结果
  • -b 运行时结果存到stderr和stdout里

unittest 支持自动搜索,这需要在后面加上子选项discover, discover可以有这些参数:

  • -s 指定文件夹
  • -t 指定模块
  • -p 模式匹配要测试的文件

unittest的一个很有用的特性是TestCasesetUp()tearDown()方法,这种方法统称"钩子"它们提供了为测试进行准备和扫尾工作的功能,听起来就像上下文管理器一样.这种功能很适合用在测试对象需要复杂执行环境的情况下.

测试用例

测试用例是指一系列相关测试的实例.如何界定这个相关程度根据实际情况而定,比如粗略的测试可以以模块为单位,一个模块的测试都放在一个测试用例中,细致的则可以是针对一个函数或者一个接口的测试都放在一个测试用例中.

unittest.TestCase是测试用例的超类,编写测试用例只要继承它即可.

%%writefile examples/utest/test_my.py

import unittest
    
from func_oper_unitest import multiply,summing

class Test_mul(unittest.TestCase):
    def setUp(self):
        pass
    def test_number_3_4(self):
        self.assertEqual(multiply(3,4),12)
    def test_string_a_3(self):
        self.assertEqual(multiply('a',3),'aaa')
        
class Test_sum(unittest.TestCase):
    def setUp(self):
        pass
    def test_number_3_4(self):
        self.assertEqual(summing(3,4),7)
    def test_number_3_4_5(self):
        self.assertEqual(summing(3,4,5),12)
class TestCase1(unittest.TestCase):
    def setUp(self):
        pass
    def test_sum_mul_2_3_mul_2_3(self):
        self.assertEqual(summing(multiply(2,3),multiply(2,3)),12)

if __name__ == '__main__':
    unittest.main()
Writing examples/utest/test_my.py
!python3 -m unittest discover -v -s ./examples/utest
test_sum_mul_2_3_mul_2_3 (test_my.TestCase1) ... ok
test_number_3_4 (test_my.Test_mul) ... ok
test_string_a_3 (test_my.Test_mul) ... ok
test_number_3_4 (test_my.Test_sum) ... ok
test_number_3_4_5 (test_my.Test_sum) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

钩子

如果要对一个模块中的每一个测试函数都做同样的初始化操作和结尾清除等操作,那么创建n个测试用例就得写n遍一样的代码.为了减少重复的代码测试用例的实例可以使用下面两个函数定义其中每个测试样例的初始化和清除工作:

  • setUp(self): 每次执行测试用例之前调用.无参数,无返回值.该方法抛出的异常都视为error而不是测试不通过.没有默认的实现.

  • tearDown(self): 每次执行测试用例之后调用.无参数,无返回值.测试方法抛出异常,该方法也正常调用,该方法抛出的异常都视为error,而不是测试不通过.只用setUp()调用成功该方法才会被调用.没有默认的实现.通过setuptesrDown组装一个module成为一个固定的测试实例.注意:如果setup运行抛出错误则测试用例代码则不会执行.但是如果setup执行成功,不管测试用例是否执行成功都会执行teardown.

测试用例也有一个类级别的钩子,它可以在测试用例类实例化之前和类全部测试实例运行完后进行操作

@classmethod
def setUpClass(cls):
    cls._connection = createExpensiveConnectionObject()

@classmethod
def tearDownClass(cls):
    cls._connection.destroy()

测试钩子也可以定义模块级别的钩子,它会在模块被引用和模块运行结束后进行工作

def setUpModule():
   createConnection()
def tearDownModule(): 
   closeConnection()
%%writefile examples/utest/test_hook.py

import unittest
    
from func_oper_unitest import multiply,summing

def setUpModule():
    print("setUpModule")
def tearDownModule(): 
    print("tearUpModule")

    
class Test_mul(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print("setUpClass")

    @classmethod
    def tearDownClass(cls):
        print("tearDownClass")
    def setUp(self):
        print("instance setUp")
    def tearDown(self):
        print("instance tearDown")
    def test_number_3_4(self):
        print("t1")
        self.assertEqual(multiply(3,4),12)
    def test_string_a_3(self):
        print("t2")
        self.assertEqual(multiply('a',3),'aaa')
        
        


if __name__ == '__main__':
    unittest.main()
Writing examples/utest/test_hook.py
!python3 -m unittest discover -v -s ./examples/utest
setUpModule
setUpClass
test_number_3_4 (test_hook.Test_mul) ... instance setUp
t1
instance tearDown
ok
test_string_a_3 (test_hook.Test_mul) ... instance setUp
t2
instance tearDown
ok
tearDownClass
tearUpModule
test_sum_mul_2_3_mul_2_3 (test_my.TestCase1) ... ok
test_number_3_4 (test_my.Test_mul) ... ok
test_string_a_3 (test_my.Test_mul) ... ok
test_number_3_4 (test_my.Test_sum) ... ok
test_number_3_4_5 (test_my.Test_sum) ... ok

----------------------------------------------------------------------
Ran 7 tests in 0.000s

OK

上面的结果可以很清洗的看到测试钩子的上下文范围.

断言

断言是用来声明结果状态的工具,如果结果符合预期,那么断言就该通过,否则断言就该报断言错误

python自带断言关键字assert

assert 1==1
assert 1==0
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

Cell In[7], line 1
----> 1 assert 1==0


AssertionError: 

自带的断言关键字功能单一,往往不能完全满足测试需要,比如要测报错的错误类型就很麻烦了,因此除assert关键字外,标准库unittest模块还有其他几个断言工具实现更加复杂的断言.

基本断言方法

基本的断言方法提供了测试结果是True还是False。所有的断言方法都有一个msg参数,如果指定msg参数的值,则将该信息作为失败的错误信息返回。

断言方法 断言描述
assertEqual(arg1, arg2, msg=None) 验证arg1=arg2,不等则fail
assertNotEqual(arg1, arg2, msg=None) 验证arg1 != arg2, 相等则fail
assertTrue(expr, msg=None) 验证expr是true,如果为false,则fail
assertFalse(expr,msg=None) 验证expr是false,如果为true,则fail
assertIs(arg1, arg2, msg=None) 验证arg1、arg2是同一个对象,不是则fail
assertIsNot(arg1, arg2, msg=None) 验证arg1、arg2不是同一个对象,是则fail
assertIsNone(expr, msg=None) 验证expr是None,不是则fail
assertIsNotNone(expr, msg=None) 验证expr不是None,是则fail
assertIn(arg1, arg2, msg=None) 验证arg1是arg2的子串,不是则fail
assertNotIn(arg1, arg2, msg=None) 验证arg1不是arg2的子串,是则fail
assertIsInstance(obj, cls, msg=None) 验证obj是cls的实例,不是则fail
assertNotIsInstance(obj, cls, msg=None) 验证obj不是cls的实例,是则fail

容器断言

判断两个容器中内容是否一样

断言方法 针对容器 断言描述
assertMultiLineEqual(a, b) strings 比较两个字符串是否一样
assertSequenceEqual(a, b) sequences 比较两个序列是否一样
assertListEqual(a, b) lists 比较两个list是否一样
assertTupleEqual(a, b) tuples 比较两个元组是否一样
assertSetEqual(a, b) sets 或者 frozensets 比较两个集合是否一样
assertDictEqual(a, b) dicts 比较两个字典是否一样

模糊比较断言

unittest框架提供的第二种断言类型就是比较断言。

下面我们看下各种比较断言:

断言方法 断言描述 参数说明
assertAlmostEqual(first, second, places=7, msg=None, delta=None) 验证first约等于second palces:指定精确到小数点后多少位,默认为7
assertNotAlmostEqual(first, second, places, msg, delta) 验证first不约等于second palces:指定精确到小数点后多少位,默认为7,注: 在上述的两个函数中如果delta指定了值则first和second之间的差值必须小于等于delta
assertGreater(first, second, msg=None) 验证first>second,否则fail ---
assertGreaterEqual(first, second, msg=None) 验证first ≥ second,否则fail ---
assertLess(first, second, msg=None) 验证first < second,否则fail ---
assertLessEqual(first, second, msg=None) 验证first ≤ second,否则fail ---
assertRegexpMatches(text, regexp, msg=None) 验证正则表达式regexp搜索==匹配==的文本text regexp:通常使用re.search()
assertNotRegexpMatches(text, regexp, msg=None) 验证正则表达式regexp搜索==不匹配==的文本text regexp:通常使用re.search()

异常断言

断言方法 断言描述
assertRaises(exc, fun, *args, **kwds) 验证异常测试,第一个参数是预期的异常,第二个则是待测的函数名,后面的则是待测函数的参数.当调用待测试函数时,在传入相应的测试数据后,如果测试通过,则表明待测试函数抛出了预期的异常,否则测试失败.
assertRaisesRegex(exc, r, fun, *args, **kwds) 验证异常测试,第一个参数是预期的异常,第二个参数则是一个用来匹配错误信息的正则表达式,第三个则是待测的函数名,后面的则是待测函数的参数.当调用待测试函数时,在传入相应的测试数据后,如果测试通过,则表明待测试函数抛出了预期的异常,且错误信息也匹配,否则测试失败.
assertWarns(warn, fun, *args, **kwds) 验证警告测试,第一个参数是预期的警告,第二个参数是待测的函数名,后面的则是待测函数的参数.当调用待测试函数时,在传入相应的测试数据后,如果测试通过,则表明待测试函数抛出了预期的警告否则测试失败.
assertWarnsRegex(warn, r, fun, *args, **kwds) 验证警告测试,第一个参数是预期的警告,第二个参数则是一个用来匹配警告信息的正则表达式,第三个则是待测的函数名,后面的则是待测函数的参数.当调用待测试函数时,在传入相应的测试数据后,如果测试通过,则表明待测试函数抛出了预期的警告,且警告信息也匹配否则测试失败.
assertLogs(logger, level) 断言log信息

异常断言与其他不太一样,往往结合上下文使用

%%writefile examples/utest/test_assert_base.py
import unittest
class demoTest(unittest.TestCase):
    def test_eq(self):
        self.assertEqual(4 + 5,9)

    def test_not_eq(self):
        self.assertNotEqual(5 * 2,10)

    def test_seq(self):
        a=["1","2","3"]
        b = ("1","2","3")
        self.assertSequenceEqual(a, b)

    def test_not_seq(self):
        a = ["1","2","3"]
        b = ("1","2","3","4")
        self.assertSequenceEqual(a, b)

    def test_almost_eq(self):
        self.assertAlmostEqual(1.003, 1.004, places = 2)

    def test_not_almost_eq(self):
        self.assertAlmostEqual(1.003, 1.004, places = 4)
        
    def test_exc(self):
        def fun():
            assert 0
        self.assertRaises(AssertionError, fun)

    def test_with_exc(self):
        def fun():
            assert 0
        with self.assertRaises(AssertionError) as a:
            fun()
Writing examples/utest/test_assert_base.py
!python3 -m unittest discover -v -s ./examples/utest
test_almost_eq (test_assert_base.demoTest) ... ok
test_eq (test_assert_base.demoTest) ... ok
test_exc (test_assert_base.demoTest) ... ok
test_not_almost_eq (test_assert_base.demoTest) ... FAIL
test_not_eq (test_assert_base.demoTest) ... FAIL
test_not_seq (test_assert_base.demoTest) ... FAIL
test_seq (test_assert_base.demoTest) ... ok
test_with_exc (test_assert_base.demoTest) ... ok
setUpModule
setUpClass
test_number_3_4 (test_hook.Test_mul) ... instance setUp
t1
instance tearDown
ok
test_string_a_3 (test_hook.Test_mul) ... instance setUp
t2
instance tearDown
ok
tearDownClass
tearUpModule
test_sum_mul_2_3_mul_2_3 (test_my.TestCase1) ... ok
test_number_3_4 (test_my.Test_mul) ... ok
test_string_a_3 (test_my.Test_mul) ... ok
test_number_3_4 (test_my.Test_sum) ... ok
test_number_3_4_5 (test_my.Test_sum) ... ok

======================================================================
FAIL: test_not_almost_eq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 23, in test_not_almost_eq
    self.assertAlmostEqual(1.003, 1.004, places = 4)
AssertionError: 1.003 != 1.004 within 4 places (0.001000000000000112 difference)

======================================================================
FAIL: test_not_eq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 7, in test_not_eq
    self.assertNotEqual(5 * 2,10)
AssertionError: 10 == 10

======================================================================
FAIL: test_not_seq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 17, in test_not_seq
    self.assertSequenceEqual(a, b)
AssertionError: Sequences differ: ['1', '2', '3'] != ('1', '2', '3', '4')

Second sequence contains 1 additional elements.
First extra element 3:
'4'

- ['1', '2', '3']
+ ('1', '2', '3', '4')

----------------------------------------------------------------------
Ran 15 tests in 0.001s

FAILED (failures=3)

跳过测试和预期测试出错

unittest提供了4个装饰器来控制测试的行为

  • @unittest.skip(reason) 跳过测试用例的某条测试项目

  • @unittest.skipIf(condition, reason) 如果条件condition条件成立,那就跳过测试用例的某条测试项目

  • @unittest.skipUnless(condition, reason) 如果条件condition条件不成立,那就跳过测试用例的某条测试项目

  • @unittest.expectedFailure 断定测试项目会失败

%%writefile examples/utest/test_assert_skip.py
import unittest
class demoTest(unittest.TestCase):
    @unittest.skip("跳过")
    def test_eq(self):
        self.assertEqual(4 + 5,9)
    @unittest.expectedFailure 
    def test_not_eq(self):
        self.assertNotEqual(5 * 2,10)
    @unittest.skipIf(1==0, "if 1 ==0")
    def test_seq(self):
        a=["1","2","3"]
        b = ("1","2","3")
        self.assertSequenceEqual(a, b)
    @unittest.skipUnless(1==0, "unless 1 ==0")
    def test_seq_2(self):
        a=["1","2","3"]
        b = ("1","2","3")
        self.assertSequenceEqual(a, b)
    @unittest.expectedFailure 
    def test_not_seq(self):
        a = ["1","2","3"]
        b = ("1","2","3","4")
        self.assertSequenceEqual(a, b)
Writing examples/utest/test_assert_skip.py
!python3 -m unittest discover -v -s ./examples/utest
test_almost_eq (test_assert_base.demoTest) ... ok
test_eq (test_assert_base.demoTest) ... ok
test_exc (test_assert_base.demoTest) ... ok
test_not_almost_eq (test_assert_base.demoTest) ... FAIL
test_not_eq (test_assert_base.demoTest) ... FAIL
test_not_seq (test_assert_base.demoTest) ... FAIL
test_seq (test_assert_base.demoTest) ... ok
test_with_exc (test_assert_base.demoTest) ... ok
test_eq (test_assert_skip.demoTest) ... skipped '跳过'
test_not_eq (test_assert_skip.demoTest) ... expected failure
test_not_seq (test_assert_skip.demoTest) ... expected failure
test_seq (test_assert_skip.demoTest) ... ok
test_seq_2 (test_assert_skip.demoTest) ... skipped 'unless 1 ==0'
setUpModule
setUpClass
test_number_3_4 (test_hook.Test_mul) ... instance setUp
t1
instance tearDown
ok
test_string_a_3 (test_hook.Test_mul) ... instance setUp
t2
instance tearDown
ok
tearDownClass
tearUpModule
test_sum_mul_2_3_mul_2_3 (test_my.TestCase1) ... ok
test_number_3_4 (test_my.Test_mul) ... ok
test_string_a_3 (test_my.Test_mul) ... ok
test_number_3_4 (test_my.Test_sum) ... ok
test_number_3_4_5 (test_my.Test_sum) ... ok

======================================================================
FAIL: test_not_almost_eq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 23, in test_not_almost_eq
    self.assertAlmostEqual(1.003, 1.004, places = 4)
AssertionError: 1.003 != 1.004 within 4 places (0.001000000000000112 difference)

======================================================================
FAIL: test_not_eq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 7, in test_not_eq
    self.assertNotEqual(5 * 2,10)
AssertionError: 10 == 10

======================================================================
FAIL: test_not_seq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 17, in test_not_seq
    self.assertSequenceEqual(a, b)
AssertionError: Sequences differ: ['1', '2', '3'] != ('1', '2', '3', '4')

Second sequence contains 1 additional elements.
First extra element 3:
'4'

- ['1', '2', '3']
+ ('1', '2', '3', '4')

----------------------------------------------------------------------
Ran 20 tests in 0.002s

FAILED (failures=3, skipped=2, expected failures=2)

使用子测验进行迭代测试

有时候我们希望对一个迭代器中的内容分别进行测试,这种时候就可以使用测试样例的subTest上下文

%%writefile examples/utest/test_subtest.py
import unittest
class demoTest(unittest.TestCase):
   
    def test_subtest(self):
        for i in range(0, 6):
            with self.subTest(i=i):
                self.assertEqual(i % 2, 0)
Writing examples/utest/test_subtest.py
!python3 -m unittest discover -v -s ./examples/utest
test_almost_eq (test_assert_base.demoTest) ... ok
test_eq (test_assert_base.demoTest) ... ok
test_exc (test_assert_base.demoTest) ... ok
test_not_almost_eq (test_assert_base.demoTest) ... FAIL
test_not_eq (test_assert_base.demoTest) ... FAIL
test_not_seq (test_assert_base.demoTest) ... FAIL
test_seq (test_assert_base.demoTest) ... ok
test_with_exc (test_assert_base.demoTest) ... ok
test_eq (test_assert_skip.demoTest) ... skipped '跳过'
test_not_eq (test_assert_skip.demoTest) ... expected failure
test_not_seq (test_assert_skip.demoTest) ... expected failure
test_seq (test_assert_skip.demoTest) ... ok
test_seq_2 (test_assert_skip.demoTest) ... skipped 'unless 1 ==0'
setUpModule
setUpClass
test_number_3_4 (test_hook.Test_mul) ... instance setUp
t1
instance tearDown
ok
test_string_a_3 (test_hook.Test_mul) ... instance setUp
t2
instance tearDown
ok
tearDownClass
tearUpModule
test_sum_mul_2_3_mul_2_3 (test_my.TestCase1) ... ok
test_number_3_4 (test_my.Test_mul) ... ok
test_string_a_3 (test_my.Test_mul) ... ok
test_number_3_4 (test_my.Test_sum) ... ok
test_number_3_4_5 (test_my.Test_sum) ... ok
test_subtest (test_subtest.demoTest) ... 
======================================================================
FAIL: test_not_almost_eq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 23, in test_not_almost_eq
    self.assertAlmostEqual(1.003, 1.004, places = 4)
AssertionError: 1.003 != 1.004 within 4 places (0.001000000000000112 difference)

======================================================================
FAIL: test_not_eq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 7, in test_not_eq
    self.assertNotEqual(5 * 2,10)
AssertionError: 10 == 10

======================================================================
FAIL: test_not_seq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 17, in test_not_seq
    self.assertSequenceEqual(a, b)
AssertionError: Sequences differ: ['1', '2', '3'] != ('1', '2', '3', '4')

Second sequence contains 1 additional elements.
First extra element 3:
'4'

- ['1', '2', '3']
+ ('1', '2', '3', '4')

======================================================================
FAIL: test_subtest (test_subtest.demoTest) (i=1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_subtest.py", line 7, in test_subtest
    self.assertEqual(i % 2, 0)


======================================================================
FAIL: test_subtest (test_subtest.demoTest) (i=3)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_subtest.py", line 7, in test_subtest
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

======================================================================
FAIL: test_subtest (test_subtest.demoTest) (i=5)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_subtest.py", line 7, in test_subtest
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

----------------------------------------------------------------------
Ran 21 tests in 0.003s

FAILED (failures=6, skipped=2, expected failures=2)

分组测试

很多时候我们希望将测试用例组织起来一部分一部分的测试而不是一次全测了,这种时候就可以使用unittest.TestSuite组织需要的测试,细化的运行测试.

注意细分的测试不能用命令行工具来运行了,而是应该独立运行脚本

%%writefile examples/utest/test_suite.py
import unittest
class ArithTest(unittest.TestCase):
    def test_eq(self):
        self.assertEqual(4 + 5,9)

    def test_not_eq(self):
        self.assertNotEqual(5 * 2,10)

    def test_seq(self):
        a=["1","2","3"]
        b = ("1","2","3")
        self.assertSequenceEqual(a, b)

    def test_not_seq(self):
        a = ["1","2","3"]
        b = ("1","2","3","4")
        self.assertSequenceEqual(a, b)



def suite():
    suite = unittest.TestSuite()
    suite.addTest(ArithTest("test_eq"))
    suite.addTest(ArithTest('test_seq'))
    return suite


if __name__ == '__main__':
    runner = unittest.TextTestRunner(verbosity=2)
    test_suite = suite()
    runner.run(test_suite)
Writing examples/utest/test_suite.py
!python3 examples/utest/test_suite.py
test_eq (__main__.ArithTest) ... ok
test_seq (__main__.ArithTest) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

测试模块最佳实践

测试驱动的开发模式中,我们需要全局测试以便了解被测试到的代码覆盖情况,也需要对每个单独的文件执行测试,用来了解针对某一接口或者功能的测试是否可以通过,因此我们需要测试模块可以全局测试也可以单测试文件测试.假设我们的测试模块文件结构如下:

test|
   |---__init__.py
   |---test_model1|
   |          |--__init__.py
   |          |--test_interface1.py
   |          |--test_interface2.py
   |
   |---test_model2|
   |          |--__init__.py
   |          |--test_interface3.py
   |
   |---main.py
     

mock测试

mock测试就是在测试过程中对于某些不容易构造或者不容易获取的对象用一个虚拟的对象来创建以便测试的测试方法.

mock测试通常有几个使用场景:

  • 调用外部接口

通常我的软件都会调用一些已有的服务,与这些服务进行交互我们才能顺利执行软件.这就引入了副作用——我们测试的时候不希望真的为了测试而这些外部服务,但不调用往往我们的业务逻辑又进展不下去.直观一点,我们希望测试一项扫码付款功能,它需要调用支付宝的付款接口,我们测试的时候当然并不希望真的调用支付宝付款,毕竟会真的产生交易,这种时候就是mock测试的用武之地了.

  • 模拟耗时步骤

比如我们要写一个代码,它会进行一步耗时1小时的计算(可以假设是使用tensorflow训练一个深度学习模型).而我们的测试代码只是希望测试出去训练模型外的业务逻辑有没有出错,这样的话就可以使用mock模拟一下这一步耗时的计算,这样就可以先调通其他逻辑,确保无误后再构建完整的代码了

  • 不修改环境的测试

比如我们的测试要删除数据库中的一条数据,而我们并不是真的要删掉,而是测试功能是否可以实现,这种时候就可以使用mock测试了

一个真实的例子

unittest模块提供了一个mock子模块,

我们以一个测试文件删除的例子来看看mock怎么工作的

待测脚本:

待测脚本可以判断path是不是文件,如果是就删除

%%writefile examples/rm/rm.py
import os
import os.path
def rm(filename):
    if os.path.isfile(filename):
        os.remove(filename)
Writing examples/rm/rm.py

测试用例:

%%writefile examples/rm/test/rm_test_1.py
from rm import rm
from unittest import mock
from unittest import TestCase

class RmTestCase(TestCase):

    @mock.patch('rm.os.path')
    @mock.patch('rm.os')
    def test_rm(self, mock_os, mock_path):
        # mock_path就是patch过后的`os.path`,mock_os就是patch过后的`os`
        #需要注意顺序
        #指定`mock_path.isfile`的返回值False
        mock_path.isfile.return_value = False

        rm("any path")        
        #测试mock_os.remove有没有被调用
        self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")        
        # 指定`mock_path.isfile`的返回值为True
        mock_path.isfile.return_value = True

        rm("any path")

        mock_os.remove.assert_called_with("any path")
Writing examples/rm/test/rm_test_1.py
!python3 -m unittest -v examples.rm.test.rm_test_1
test_rm (examples.rm.test.rm_test_1.RmTestCase) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

mock.patch装饰器将指定的对象替换为mock,其实质就是猴子补丁.

利用patch装饰器将原始对象替换为mock对象,并将替换后的对象作为参数传入测试用例的测试项中

指定mock行为

mock对象是假的,但他可以模拟真实行为,以下是它的接口:

from unittest.mock import Mock
mock = Mock()

断言行为

  • assert_called

断言方法被调用过至少一次

mock.method.assert_called()
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

Cell In[21], line 1
----> 1 mock.method.assert_called()


File ~/anaconda3/lib/python3.10/unittest/mock.py:898, in NonCallableMock.assert_called(self)
    895 if self.call_count == 0:
    896     msg = ("Expected '%s' to have been called." %
    897            (self._mock_name or 'mock'))
--> 898     raise AssertionError(msg)


AssertionError: Expected 'method' to have been called.
mock.method()
<Mock name='mock.method()' id='140328004052080'>
mock.method.assert_called()
  • assert_called_once

断言方法只被调用过一次(3.6)

mock.method.assert_called_once()
mock.method()
<Mock name='mock.method()' id='140328004052080'>
mock.method.assert_called_once()
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

Cell In[26], line 1
----> 1 mock.method.assert_called_once()


File ~/anaconda3/lib/python3.10/unittest/mock.py:908, in NonCallableMock.assert_called_once(self)
    903 if not self.call_count == 1:
    904     msg = ("Expected '%s' to have been called once. Called %s times.%s"
    905            % (self._mock_name or 'mock',
    906               self.call_count,
    907               self._calls_repr()))
--> 908     raise AssertionError(msg)


AssertionError: Expected 'method' to have been called once. Called 2 times.
Calls: [call(), call()].
  • assert_called_with(*args, **kwargs)

断言方法被调用过且调用时的参数为填入的参数

mock.method(1,2,k=4)
<Mock name='mock.method()' id='140328004052080'>
mock.method.assert_called_with(1, 2,k=4)
  • assert_called_once_with(*args, **kwargs)

断言方法只被调用过一次且调用时的参数为填入的参数

  • assert_any_call(*args, **kwargs)

断言对象有方法被用参数调用过调用过

  • assert_has_calls(calls, any_order=False)

断言某些结果是被mock对象产出的

mock = Mock(return_value=None)
call1 = mock(1)
call2 = mock(2)
call3 = mock(3)
call4 = mock(4)
mock.mock_calls# 查看mock对象的call对象
[call(1), call(2), call(3), call(4)]
call1 = mock.mock_calls[0]
call1
call(1)
mock.assert_has_calls(mock.mock_calls[:2])
mock.assert_has_calls(mock.mock_calls, any_order=True)
  • assert_not_called()

断言mock对象没被调用过

设置行为

  • reset_mock(*, return_value=False, side_effect=False)

重置mock对象,side_effect翻译为副作用,可以设置一个异常

  • mock_add_spec(spec, spec_set=False)

为mock设置规范,来定义mock对象内部的属性(attribute)

  • attach_mock(mock, attribute)

为mock设置一个mock对象在它上面

  • configure_mock(**kwargs)

设置mock

attrs = {'method.return_value': 3, 'other.side_effect': KeyError}
mock.configure_mock(**attrs)
mock.method()
3
mock.other()
---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

Cell In[34], line 1
----> 1 mock.other()


File ~/anaconda3/lib/python3.10/unittest/mock.py:1114, in CallableMixin.__call__(self, *args, **kwargs)
   1112 self._mock_check_sig(*args, **kwargs)
   1113 self._increment_mock_call(*args, **kwargs)
-> 1114 return self._mock_call(*args, **kwargs)


File ~/anaconda3/lib/python3.10/unittest/mock.py:1118, in CallableMixin._mock_call(self, *args, **kwargs)
   1117 def _mock_call(self, /, *args, **kwargs):
-> 1118     return self._execute_mock_call(*args, **kwargs)


File ~/anaconda3/lib/python3.10/unittest/mock.py:1173, in CallableMixin._execute_mock_call(self, *args, **kwargs)
   1171 if effect is not None:
   1172     if _is_exception(effect):
-> 1173         raise effect
   1174     elif not _callable(effect):
   1175         result = next(effect)


KeyError: 
  • return_value

设置mock的返回值

mock = Mock()
mock.return_value = 'fish'
mock()
'fish'
  • side_effect

设置一个副作用,也就是异常

mock = Mock()
mock.side_effect = Exception('Boom!')


mock()
---------------------------------------------------------------------------

Exception                                 Traceback (most recent call last)

Cell In[36], line 5
      1 mock = Mock()
      2 mock.side_effect = Exception('Boom!')
----> 5 mock()


File ~/anaconda3/lib/python3.10/unittest/mock.py:1114, in CallableMixin.__call__(self, *args, **kwargs)
   1112 self._mock_check_sig(*args, **kwargs)
   1113 self._increment_mock_call(*args, **kwargs)
-> 1114 return self._mock_call(*args, **kwargs)


File ~/anaconda3/lib/python3.10/unittest/mock.py:1118, in CallableMixin._mock_call(self, *args, **kwargs)
   1117 def _mock_call(self, /, *args, **kwargs):
-> 1118     return self._execute_mock_call(*args, **kwargs)


File ~/anaconda3/lib/python3.10/unittest/mock.py:1173, in CallableMixin._execute_mock_call(self, *args, **kwargs)
   1171 if effect is not None:
   1172     if _is_exception(effect):
-> 1173         raise effect
   1174     elif not _callable(effect):
   1175         result = next(effect)


Exception: Boom!

mock状态监控

  • called

是否被调用过

  • call_count

被调用计数

  • call_args

mock的最近的call对象

mock = Mock(return_value=None)
print(mock.call_args)
None
mock()
mock.call_args
call()
mock.call_args == ()
True
mock(3, 4)
mock.call_args
call(3, 4)
mock.call_args == ((3, 4),)
True
mock(3, 4, 5, key='fish', next='w00t!')
mock.call_args
call(3, 4, 5, key='fish', next='w00t!')
  • call_args_list

mock的call对象列表

mock.call_args_list
[call(), call(3, 4), call(3, 4, 5, key='fish', next='w00t!')]
  • method_calls

mock对象中的方法调用情况

mock.method_calls
[]
mock.method1()
<Mock name='mock.method1()' id='140328005952480'>
mock.method_calls
[call.method1()]
  • mock_calls

mock自身调用和其方法调用情况

mock.mock_calls
[call(), call(3, 4), call(3, 4, 5, key='fish', next='w00t!'), call.method1()]

*伪造数据

mock只能伪造接口行为,但很多时候我们测试的问题是没有数据.伪造数据可以使用faker这个包.

这个包可以模拟

  • 地址
  • 车牌号
  • 银行信息
  • 条形码
  • 颜色
  • 公司信息
  • 信用卡信息
  • 货币信息
  • 时间
  • 文件名
  • 地理信息
  • 互联网相关信息
  • 国际标准书号
  • 工作信息
  • 文章内容
  • 加密或编码信息
  • 人物信息
  • 电话号码信息
  • 个人信息
  • python容器内容
  • 社会安全码
  • http请求的user_agent

同时也支持非英语伪造信息,我们可以简单的在实例初始化时传入语言参数来变更为对应的语言

from faker import Faker
fake_en = Faker()
fake_cn = Faker('zh_CN')
fake_en.profile()
{'job': 'Development worker, international aid',
 'company': 'Mclaughlin-Long',
 'ssn': '127-56-4277',
 'residence': '2816 Angelica Branch\nJohnsonstad, KS 87015',
 'current_location': (Decimal('-20.6507845'), Decimal('64.226983')),
 'blood_group': 'AB-',
 'website': ['https://www.wright-thompson.com/',
  'http://www.carter.com/',
  'http://castro.org/'],
 'username': 'jacob51',
 'name': 'Susan Jones',
 'sex': 'F',
 'address': '036 Meagan Spurs Apt. 765\nMeganhaven, KY 43401',
 'mail': 'campbellcynthia@hotmail.com',
 'birthdate': datetime.date(1986, 3, 7)}
fake_cn.profile()
{'job': '艺术/设计',
 'company': '富罳网络有限公司',
 'ssn': '442000197302066642',
 'residence': '江西省南京县平山兰州街D座 302746',
 'current_location': (Decimal('-20.6157775'), Decimal('-111.254788')),
 'blood_group': 'AB+',
 'website': ['https://25.cn/',
  'http://www.xue.cn/',
  'https://hehe.cn/',
  'http://qinzheng.cn/'],
 'username': 'qiang18',
 'name': '陈秀英',
 'sex': 'M',
 'address': '云南省杭州市萧山张街X座 569975',
 'mail': 'jxie@gmail.com',
 'birthdate': datetime.date(1940, 1, 7)}

测试覆盖率

测试通过往往也并不能保证程序一定稳健可靠,因为测试很有可能没有覆盖全面.这时候测试覆盖率就成了评价一个项目可靠程度的标准之一了.

python标准库没有提供覆盖率统计工具,但coverage.py已经是实质上的覆盖率标准工具了.

coverage的使用有两步:

  • 使用coverage脚本的run命令执行脚本
!python3 -m coverage run examples/utest/test_suite.py
test_eq (__main__.ArithTest) ... ok
test_seq (__main__.ArithTest) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
  • 使用report/html命令输出覆盖率结果
!coverage report
Name                           Stmts   Miss  Cover
--------------------------------------------------
examples/utest/test_suite.py      23      4    83%
--------------------------------------------------
TOTAL                             23      4    83%

如果是针对全局的测试覆盖率,那么这样用:

!python3 -m coverage run --source=examples/utest -m unittest discover -v -s ./examples/utest
test_almost_eq (test_assert_base.demoTest) ... ok
test_eq (test_assert_base.demoTest) ... ok
test_exc (test_assert_base.demoTest) ... ok
test_not_almost_eq (test_assert_base.demoTest) ... FAIL
test_not_eq (test_assert_base.demoTest) ... FAIL
test_not_seq (test_assert_base.demoTest) ... FAIL
test_seq (test_assert_base.demoTest) ... ok
test_with_exc (test_assert_base.demoTest) ... ok
test_eq (test_assert_skip.demoTest) ... skipped '跳过'
test_not_eq (test_assert_skip.demoTest) ... expected failure
test_not_seq (test_assert_skip.demoTest) ... expected failure
test_seq (test_assert_skip.demoTest) ... ok
test_seq_2 (test_assert_skip.demoTest) ... skipped 'unless 1 ==0'
setUpModule
setUpClass
test_number_3_4 (test_hook.Test_mul) ... instance setUp
t1
instance tearDown
ok
test_string_a_3 (test_hook.Test_mul) ... instance setUp
t2
instance tearDown
ok
tearDownClass
tearUpModule
test_sum_mul_2_3_mul_2_3 (test_my.TestCase1) ... ok
test_number_3_4 (test_my.Test_mul) ... ok
test_string_a_3 (test_my.Test_mul) ... ok
test_number_3_4 (test_my.Test_sum) ... ok
test_number_3_4_5 (test_my.Test_sum) ... ok
test_subtest (test_subtest.demoTest) ... test_eq (test_suite.ArithTest) ... ok
test_not_eq (test_suite.ArithTest) ... FAIL
test_not_seq (test_suite.ArithTest) ... FAIL
test_seq (test_suite.ArithTest) ... ok

======================================================================
FAIL: test_not_almost_eq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 23, in test_not_almost_eq
    self.assertAlmostEqual(1.003, 1.004, places = 4)
AssertionError: 1.003 != 1.004 within 4 places (0.001000000000000112 difference)

======================================================================
FAIL: test_not_eq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 7, in test_not_eq
    self.assertNotEqual(5 * 2,10)
AssertionError: 10 == 10

======================================================================
FAIL: test_not_seq (test_assert_base.demoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_assert_base.py", line 17, in test_not_seq
    self.assertSequenceEqual(a, b)
AssertionError: Sequences differ: ['1', '2', '3'] != ('1', '2', '3', '4')

Second sequence contains 1 additional elements.
First extra element 3:
'4'

- ['1', '2', '3']
+ ('1', '2', '3', '4')

======================================================================
FAIL: test_subtest (test_subtest.demoTest) (i=1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_subtest.py", line 7, in test_subtest
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0


FAIL: test_subtest (test_subtest.demoTest) (i=3)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_subtest.py", line 7, in test_subtest
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

======================================================================
FAIL: test_subtest (test_subtest.demoTest) (i=5)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_subtest.py", line 7, in test_subtest
    self.assertEqual(i % 2, 0)
AssertionError: 1 != 0

======================================================================
FAIL: test_not_eq (test_suite.ArithTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_suite.py", line 7, in test_not_eq
    self.assertNotEqual(5 * 2,10)
AssertionError: 10 == 10

======================================================================
FAIL: test_not_seq (test_suite.ArithTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mac/WORKSPACE/GITHUB/BLOG/TutorialForPython/src/工具链篇/examples/utest/test_suite.py", line 17, in test_not_seq
    self.assertSequenceEqual(a, b)
AssertionError: Sequences differ: ['1', '2', '3'] != ('1', '2', '3', '4')

Second sequence contains 1 additional elements.
First extra element 3:
'4'

- ['1', '2', '3']
+ ('1', '2', '3', '4')

----------------------------------------------------------------------
Ran 25 tests in 0.004s

FAILED (failures=8, skipped=2, expected failures=2)

其中--source=用于指定要测试覆盖率的文件夹

!python3 -m coverage report 
Name                                  Stmts   Miss  Cover
---------------------------------------------------------
examples/utest/func_oper_unitest.py       7      0   100%
examples/utest/test_assert_base.py       27      0   100%
examples/utest/test_assert_skip.py       23      4    83%
examples/utest/test_hook.py              25      1    96%
examples/utest/test_my.py                23      1    96%
examples/utest/test_subtest.py            6      0   100%
examples/utest/test_suite.py             23      7    70%
---------------------------------------------------------
TOTAL                                   134     13    90%

*其他测试矿建

当然了还有其他的知名测试框架比如nose,pytest等,都很值得尝试.但个人觉得标准库的测试框架已经相当够用,结合coverage.py,也很简单直接.少即是多,我们的项目应该尽量减少第三方依赖.

如果一定要用第三方测试库还是更加推荐pytest,因为它目前更新维护还算比较积极,社区用的人也比较多.