unittest 框架

unittest是Python内置的单元测试框架,不仅可以完成单元测试,也适用于自动化测试中,其提供了丰富的断言方法,判断测试用例是否通过,最终生成测试结果报告。

unittest重点

学习重点在于掌握unittest中的几个关键的类:

  • unittest.TestCase : 测试用例的基类,指定测试用例方法名字,返回测试用例实例
  • unittest.TestSuite : 测试用例的容器(套件),可理解为测试集合,支持测试的用例添加和删除
  • unittest.TextTestRunner : 测试用例的执行器,以文本形式将保存到TextTestResult中
  • unittest.TestLoader : 测试用例的装载器,将测试用例(TestCase)装载到测试容器(TestSuite)中。
  • unittest.defaultTestLoader :默认装载器,本质是TestLoader装载器的实例化,与TestLoader 等同
  • unittest.TestProgram:TestProgram类名被赋值给了main变量,然后通过unittest.main()的形式调用

unittest.TestCase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import unittest  # 导入unittest框架

class myUnitTest(unittest.TestCase):

def setUp(self):
""" 用例初始化 """
print("用例初始化 setup")
def runTest(self):
""" 执行用例 """
# 用例书写位置
assert 1 == 1
def tearDown(self):
""" 用例执行完,收尾 """
print("用例执行完毕,收尾")
if __name__ == '__main__':
mytest = myUnitTest()
mytest.run() # 固定的调用方法run

从上述代码分析执行流程,首先执行用例前会使用setUp进行初始化操作,然后执行测试用例,最终使用tearDown进行收尾工作

注意:

  • myUnitTest类名可以自定义,但是必须继承unittest.TestCase
  • setUp和tearDown方法名是固定的,测试用例时,如果没有初始化和收尾的工作,setUp和tearDown方法可以省略不写。
  • 在每个用例执行时,setUp和tearDown都会执行

为什么实例化了myUnitTest调用run方法就执行了,原因当然要从其继承的父类里找问题,可以看到

1
2
3
4
5
6
7
8
9
10
class TestCase(object):

def __init__(self, methodName='runTest'):
self._testMethodName = methodName
self._outcome = None
self._testMethodDoc = 'No test' # 也请留意这个鬼东西 No test

def run(self, result=None):
# run方法反射了methodName
testMethod = getattr(self, self._testMethodName)

在TestCase实例化的时候,有个methodName默认参数,默认为runTest。而在实例化后,实例化对象调用run方法的时候,反射了那个methodName值,所以用例执行了

因此引出我们可以通过传参methodName来自定义我们用例的名称

1
2
mytest = myUnitTest(methodName='new_test')
mytest.run()

但是这种原始的写法也太low了,一般是不会这样写的。

unittest断言

使用框架测试,其最重要的就是使用断言来判断用例是否预期与实际匹配

unittet.TestCase提供了一些断言方法用来检查并报告故障。

方法 类比 描述 版本
assertEqual(a, b, msg) a == b 如果a不等于b,断言失败
assertNotEqual(a, b, msg) a != b 如果a等于b,断言失败
assertTrue(x, msg) bool(x) is True 如果表达式x不为True,断言失败
assertFalse(x, msg) bool(x) is False 如果表达式x不为False,断言失败
assertIs(a, b, msg) a is b 如果a is not 2,断言失败 3.1
assertIsNot(a, b, msg) a is not b 如果a is b,断言失败 3.1
assertIsNone(x, msg) x is not None 如果x不是None,断言失败 3.1
assertIn(a, b, msg) a in b 如果a not in b,断言失败 3.1
assertNotIn(a, b, msg) a not in b 如果a in b,断言失败 3.1
assertIsInstance(a, b, msg) isinstance(a, b) 如果a不是b类型,断言失败 3.2
assertNotIsInstance(a, b, msg) not isinstance(a, b) 如果a是b类型,断言失败 3.2

注:assert方法都接收一个msg参数,如果指定,该参数将用作失败时的错误提示

unittest.TestSuite

测试套件(test suite)是由许多测试用例组成的复合测试,也可以理解为承载多个用例集合的容器。
使用时需要创建一个TestSuite实例对象,然后使用该对象添加用例:

  • suite_obj.addTest(self, test),添加一个测试用例。
  • suite_obj.addTests(self, tests),添加多个测试用例。
  • 在实例化方法中添加测试用例。

其有效地将多个用例组织到一起进行集中测试,解决了单个用例测试的问题。

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 实例化suite对象
suite = unittest.TestSuite()
# 添加用例
suite.addTest("case1")
suite.addTest("case2")

# 可以查看suite中的用例数量
print(suite.countTestCases())

# 拿到执行器对象
runner = unittest.TextTestRunner()
# 执行器执行容器中的用例
runner.run(suite)

一个一个添加用例未免太傻了,使用python高阶函数

1
2
3
map_obj = map(myUnitTest, ['case1', 'case2'])
suite_obj = unittest.TestSuite()
suite_obj.addTests(map_obj)

实例化时候添加用例

1
2
map_obj = map(myUnitTest, ['case1', 'case2'])
suite_obj = unittest.TestSuite(tests=map_obj)

以上都需要手动的将用例添加到suite的中,太累了,自动添加

unittest.MakeSuite

想要自动添加,需要使用unittest.makeSuite类来完成,在实例化unittest.makeSuite(testCaseClass, prefix='test')时,需要告诉makeSuite添加用例的类名,然后makeSuite将testCaseClass类中所有以prefix参数指定开头的用例(默认读取以test开头),自动添加到suite中。

1
suite_obj = unittest.makeSuite(myUnitTest, prefix='xxx')

当然也可以同时手动补充用例

1
2
suite_obj = unittest.makeSuite(myUnitTest, prefix='xxx')
suite_obj.addTests(map(myUnitTest, ['case3', 'case4']))

unittest.TestLoader

以上其所有的用例方法都封装在一个用例类中,但实际中我们会根据不同的功能编写不同的测试用例文件,存放在不同的目录内

这个时用addTest添加就非常的麻烦了,unittest提供了TestLoader类来解决这个问题。

TestLoader提供的方法:

  • TestLoader.loadTestsFromTestCase,返回testCaseClass中包含的所有测试用例的suite。
  • TestLoader.loadTestsFromModule,返回包含在给定模块中的所有测试用例的suite。
  • TestLoader.loadTestsFromName,返回指定字符串的所有测试用例的suite。
  • TestLoader.loadTestsFromNames,返回指定序列中的所有测试用例suite。
  • TestLoader.discover,从指定的目录开始递归查找所有测试模块。

拿工作中常用的来写,TestLoader.discover

discover的语法:

1
2
3
4
5
6
discover = unittest.TestLoader().discover(
start_dir=base_dir, # 该参必传
pattern='test*.py', # 保持默认即可
top_level_dir=None
)
unittest.TextTestRunner(verbosity=2).run(discover)

通过TestLoader()实例化对象,然后通过实例化对象调用discover方法,discover根据给定目录,递归找到子目录下的所有符合规则的测试模块,然后交给TestSuit生成用例集suite,最后交给TextTestRunner执行用例。
该discover方法接收的三个参数:

  • start_dir:要测试的模块名或者测试用例的目录
  • pattern=”test*.py”:表示用例文件名的匹配原则,默认匹配以test开头的文件名,星号表示后续的多个字符
  • top_level_dir=None:测试模块的顶层目录,如果没有顶层目录,默认为None

:discover对给定的目录是有要求的,它只识别Python的包,也就是目录内有__init__.py文件的才算是Python的包,只要是要读取的目录,都必须是包。

关于start_dir和top_level_dir的几种情况:

  • start_dir目录可以单独指定,这个时候,让top_level_dir保持默认(None)即可。
  • start_dir == top_level_dir, start_dir目录与top_level_dir目录一致,discover寻找start_dir指定目录内的符合规则的模块。
  • start_dir < top_level_dir,start_dir目录是top_level_dir目录的子目录。discover寻找start_dir指定目录内的符合规则的模块。
  • start_dir > top_level_dir,start_dir目录如果大于top_level_dir目录,等待你的是报错AssertionError: Path must be within the project。说你指定的路径(start_dir)必须位于项目内(top_level_dir)。

另外unittest已经帮我们实例化好了TestLoader对象—>defaultTestLoader,我们可以直接使用defaultTestLoader.discover

1
2
3
4
5
6
discover = unittest.defaultTestLoader.discover(
start_dir=base_dir,
pattern='test*.py',
top_level_dir=base_dir
)
unittest.TextTestRunner(verbosity=2).run(discover)
都看到这里了,不赏点银子吗^v^