About

 

unittest是Python内置的单元测试框架(模块),不仅可以完成单元测试,也适用于web自动化测试中。

unittest提供了丰富的断言方法,判断测试用例是否通过,然后生成测试结果报告。

必要的准备与注意事项

 

首先,我们准备这样一个目录:

M:\tests\  # 我的是M盘的tests目录,所有操作都在tests目录内完成
├─discover
│ ├─son
│ │ ├─test_dict.py
│ │ └─__init__.py
│ ├─test_list.py
│ ├─test_str.py
│ └─__init__.py
├─loadTestsFromTestCaseDemo
│ └─loadTestsFromTestCaseDemo.py
├─case_set.py
├─myMain.py # 代码演示文件,所有演示脚本文件
├─test_tuple.py
└─__init__.py

如果你跟我的流程走, 请务必建立和理解这样的一个目录,目前这些文件都是空的,后续会一一建立,各目录内的​​__init__.py​​也必须建立,虽然它是空的,但是它无比重要,因为它标明它所在目录是Python的包。

​case_set.py​​有4个函数,分别计算加减乘除,并且代码不变:

"""
用例集
"""


def add(x, y):
""" 两数相加 """
return x + y


def sub(x, y):
""" 两数相减 """
return x - y


def mul(x, y):
""" 两数相乘 """
return x * y


def div(x, y):
""" 两数相除 """
return x / y


if __name__ == '__main__':
print(div(10, 5))
print(div(10, 0))

上述4个函数将成为我们的测试用例。

另外,示例演示环境是:

python3.6 + windows10 + pycharm2018.1

注意!注意!!注意!!!

如果你对pycharm的使用烂熟于心,那么在运行接下来的示例时,请不要右键运行或者点击运行按钮执行脚本,而是通过​​Terminal​​或者终端执行脚本,因为pycharm的集成环境会影响测试结果。

unittest简单上手

 

runTest

import unittest  # 导入unittest框架
import case_set # 导入用例集

class myUnitTest(unittest.TestCase):

def setUp(self):
""" 用例初始化 """
print("用例初始化 setup")
def runTest(self):
""" 执行用例 """
print(case_set.add(2, 3) == 5)
def tearDown(self):
""" 用例执行完,收尾 """
print("用例执行完毕,收尾")
if __name__ == '__main__':
demo = myUnitTest()
demo.run() # 固定的调用方法run

执行结果:

Ran 1 test in 0.002s

OK
用例初始化 setup
True
用例执行完毕,收尾

由结果可以看到,1个用例在多少时间内执行完毕,并且用例执行通过。

用例的执行流程是:

  • setUp先开第一枪,处理一些初始化操作。
  • 接着runTest执行用例,用例返回True。
  • 最后,tearDown打扫战场!

在每个用例执行时,setUp和tearDown都会执行。

注意:

  • myUnitTest类名可以自定义,但是必须继承​​unittest.TestCase​​。
  • 示例中的setUp和tearDown方法名是固定的,但如果,我们测试用例时,没有初始化和收尾的工作,setUp和tearDown方法可以省略不写。

至于runTest方法名叫什么,取决于在实例化myUnitTest类时,是否传参,我们来看​​unittest.TestCase​​​类的​​__init__​​方法和run方法做了什么:

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)

可以看到,在实例化的时候,其实有个​​methodName​​​默认参数,正好也叫runTest。而在实例化后,实例化对象调用run方法的时候,反射了那个​​methodName​​值,然后用例正常执行了。

所以,runTest方法名可以自定义:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
print(case_set.add(2, 3) == 5)

if __name__ == '__main__':
demo = myUnitTest(methodName='add_test')
demo.run()

执行多个用例

那么,如果要执行多个用例怎么办?

import unittest
import case_set

class myUnitTestAdd(unittest.TestCase):

def runTest(self):
""" 执行用例 """
print(case_set.add(2, 3) == 5)

class myUnitTestSub(unittest.TestCase):

def runTest(self):
""" 执行用例 """
print(case_set.sub(2, 3) == 5) # 用例结果不符合预期

if __name__ == '__main__':
demo1 = myUnitTestAdd()
demo2 = myUnitTestSub()
demo1.run()
demo2.run()

上面的示例,也可以写成:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
print(case_set.add(2, 3) == 5)

def sub_test(self):
""" 执行用例"""
print(case_set.sub(10, 5) == 2)

if __name__ == '__main__':
demo1 = myUnitTest('add_test')
demo2 = myUnitTest('sub_test')
demo1.run()
demo2.run()

如上方式,每个用例都要实例化一次,虽然可以执行多个用例,但是这么写实在是太low了,反倒没有之前测试除法用例来的简单。

另外,用print打印也不符合真实的测试环境。

我们先来解决print的问题。

使用unittest提供的断言

 

来看看unittest为我们提供了哪些断言方法吧!

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

下表列出了最常用的方法:

Method

Checks that

description

New in

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

示例:

import unittest


class TestStringMethods(unittest.TestCase):

def test_assertEqual(self):
self.assertEqual(1, 2, msg='1 != 2') # AssertionError: 1 != 2 : 1 != 2

def test_assertTrue(self):
self.assertTrue('')

def test_assertFalse(self):
self.assertFalse('')


if __name__ == '__main__':
unittest.main()

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

结果示例:

F.F
======================================================================
FAIL: test_assertEqual (__main__.TestStringMethods)
----------------------------------------------------------------------
Traceback (most recent call last):
File "myMain.py", line 251, in test_assertEqual
self.assertEqual(1, 2, msg='1 != 2') # AssertionError: 1 != 2 : 1 != 2
AssertionError: 1 != 2 : 1 != 2

======================================================================
FAIL: test_assertTrue (__main__.TestStringMethods)
----------------------------------------------------------------------
Traceback (most recent call last):
File "myMain.py", line 254, in test_assertTrue
self.assertTrue('')
AssertionError: '' is not true

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=2)

结果中,​​F.F​​​表示,如果用例通过返回​​.​​​,失败返回​​F​​​,所以结果告诉我们执行了3个用例,成功1个,失败两个​​FAILED (failures=2)​​​,​​AssertionError​​是错误信息。

unittest.TestSuite

 

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

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

当添加完所有用例后,该测试套件将被交给测试执行(运行)器,如TextTestRunner,该执行器会按照用例的添加顺序执行各用例,并聚合结果。

TestSuite有效的解决了:

  • 因为是顺序执行,当多个用例组成一个链式测试操作时,谁先谁后的问题就不存在了。
  • 有效地将多个用例组织到一起进行集中测试,解决了之前一个一个测试的问题。

suite_obj.addTest(self, test)

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
""" 执行用例"""
self.assertEqual(case_set.sub(10, 5), 5)

def create_suite():
""" 创建用例集 """
# 拿到两个用例对象
add = myUnitTest('add_test')
sub = myUnitTest('sub_test')
# 实例化suite对象
suite_obj = unittest.TestSuite()
# 添加用例
suite_obj.addTest(add)
suite_obj.addTest(sub)
return suite_obj

if __name__ == '__main__':
suite = create_suite()
# 可以查看suite中的用例数量
# print(suite.countTestCases()) # 2
# 拿到执行器对象
runner = unittest.TextTestRunner()
# 你想用执行器执行谁?就把它传进去
runner.run(suite)

代码注释已经说得很明白了,只需要记住可以通过​​suite.countTestCases()​​方法获取suite中用例的数量。

suite_obj.addTests(self, tests)

一个一个往suite中添加用例比较麻烦,所以,再来个简单的:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
""" 执行用例"""
self.assertEqual(case_set.sub(10, 5), 5)


def create_suite():
""" 创建用例集 """
'''
# 拿到两个用例对象
add = myUnitTest('add_test')
sub = myUnitTest('sub_test')
# 实例化suite对象
suite_obj = unittest.TestSuite()
# 添加用例
suite_obj.addTests([add, sub])
'''
# 上面的代码也可以这么写
map_obj = map(myUnitTest, ['add_test', 'sub_test'])
suite_obj = unittest.TestSuite()
suite_obj.addTests(map_obj)
return suite_obj

if __name__ == '__main__':

suite = create_suite()
# 可以查看suite中的用例数量
# print(suite.countTestCases()) # 2
# 拿到执行器对象
runner = unittest.TextTestRunner()
# 你想用执行器执行谁?就把它传进去
runner.run(suite)

实例化时添加用例

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
""" 执行用例"""
self.assertEqual(case_set.sub(10, 5), 5)

def create_suite():
""" 创建用例集 """
map_obj = map(myUnitTest, ['add_test', 'sub_test'])
suite_obj = unittest.TestSuite(tests=map_obj)
return suite_obj

if __name__ == '__main__':
suite = create_suite()
runner = unittest.TextTestRunner()
runner.run(suite)

怎么玩的呢?其实我们在实例化时做了添加用例的操作,以下示例演示了实例化的过程:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
""" 执行用例"""
self.assertEqual(case_set.sub(10, 5), 5)

class myUnitTestSuite(unittest.TestSuite):
def __init__(self):
# 当实例化suite对象时,传递用例
map_obj = map(myUnitTest, ['add_test', 'sub_test'])
# 调用父类的 __init__ 方法
super().__init__(tests=map_obj)

if __name__ == '__main__':
suite_obj = myUnitTestSuite()
runner = unittest.TextTestRunner()
runner.run(suite_obj)

虽然在一定程度上,我们优化了代码,但是还不够,因为,我们还需要手动的将用例添加到suite的中。接下来,我们来学习,如何自动添加。

unittest.makeSuite

 

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
self.assertEqual(case_set.sub(10, 5), 2)

def test_mul(self):
self.assertEqual(case_set.mul(10, 5), 50)

def test_div(self):
self.assertEqual(case_set.div(10, 5), 2)

def create_suite():
""" 创建用例集 """
suite_obj = unittest.makeSuite(testCaseClass=myUnitTest, prefix='test')
return suite_obj

if __name__ == '__main__':
suite_obj = create_suite()
print(suite_obj.countTestCases()) # 2
runner = unittest.TextTestRunner()
runner.run(suite_obj)

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

再次强调,prefix参数默认读取以test开头的用例,也可以自己指定:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def my_add_test(self):
self.assertEqual(case_set.add(2, 3), 5)

def my_sub_test(self):
self.assertEqual(case_set.sub(10, 5), 2) # AssertionError: 5 != 2

def test_mul(self):
self.assertEqual(case_set.mul(10, 5), 50)

def test_div(self):
self.assertEqual(case_set.div(10, 5), 2)

def create_suite():
""" 创建用例集 """
suite_obj = unittest.makeSuite(myUnitTest, prefix='my')
return suite_obj

if __name__ == '__main__':
suite_obj = create_suite()
print(suite_obj.countTestCases()) # 2
runner = unittest.TextTestRunner()
runner.run(suite_obj)

如上例示例,读取myUnitTest类中所有以​​my​​开头的用例方法。但建议还是按照人家默认的test就好了。

除此之外,这都9102年了, 车车都是手自一体的,咱们除了能玩自动添加,也能手动的将指定的用例添加到suite中:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def my_add_test(self):
self.assertEqual(case_set.add(2, 3), 5)

def my_sub_test(self):
self.assertEqual(case_set.sub(10, 5), 2) # AssertionError: 5 != 2

def test_mul(self):
self.assertEqual(case_set.mul(10, 5), 50)

def test_div(self):
self.assertEqual(case_set.div(10, 5), 2)

def create_suite():
""" 创建用例集 """
suite_obj = unittest.makeSuite(myUnitTest, prefix='my')
suite_obj.addTests(map(myUnitTest, ['test_mul', 'test_div']))
return suite_obj

if __name__ == '__main__':
suite_obj = create_suite()
print(suite_obj.countTestCases()) # 4
runner = unittest.TextTestRunner()
runner.run(suite_obj)

上例,使用makeSuite自动添加所有以​​my​​开头的用例,然后又使用addTests添加两个用例。

unittest.TestLoader

 

到目前为止,我们所有的用例方法都封装在一个用例类中,但是有的时候,我们会根据不同的功能编写不同的测试用例文件,甚至是存放在不同的目录内。

这个时候在用addTest添加就非常的麻烦了。
unittest提供了TestLoader类来解决这个问题。先看提供了哪些方法:

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

执行脚本文件为​​myMain.py​​​,目录结构,参见开头的目录结构示例。
TestLoader.loadTestsFromTestCase

首先,​​loadTestsFromTestCaseDemo.py​​代码如下:

import unittest

class LoadTestsFromTestCaseDemo(unittest.TestCase):

def test_is_upper(self):
self.assertTrue('FOO'.isupper())

def test_is_lower(self):
self.assertTrue('foo'.islower())

​LoadTestsFromTestCaseDemo​​类中有两个测试用例。

import unittest
from loadTestsFromTestCaseDemo.loadTestsFromTestCaseDemo import LoadTestsFromTestCaseDemo

class MyTestCase(unittest.TestCase):

def test_upper(self):
self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
# 使用loadTestsFromTestCase获取当前脚本和loadTestsFromTestCaseDemo脚本中的用例类
test_case1 = unittest.TestLoader().loadTestsFromTestCase(MyTestCase)
test_case2 = unittest.TestLoader().loadTestsFromTestCase(LoadTestsFromTestCaseDemo)
# 创建suite并添加用例类
suite = unittest.TestSuite()
suite.addTests([test_case1, test_case2])
unittest.TextTestRunner(verbosity=2).run(suite)

上例中,​​loadTestsFromTestCase​​需要传入用例类的类名。无所谓这个用例类所处的目录或者文件。

TestLoader.loadTestsFromModule

​loadTestsFromTestCaseDemo.py​​代码稍微有些变动:

import unittest

class LoadTestsFromTestCaseDemo1(unittest.TestCase):

def test_is_upper(self):
self.assertTrue('FOO'.isupper())

def test_is_lower(self):
self.assertTrue('foo'.islower())

class LoadTestsFromTestCaseDemo2(unittest.TestCase):

def test_startswith(self):
self.assertTrue('FOO'.startswith('F'))

def test_endswith(self):
self.assertTrue('foo'.endswith('o'))

再来看​​maMain.py​​:

import unittest
from loadTestsFromTestCaseDemo import loadTestsFromTestCaseDemo

class MyTestCase(unittest.TestCase):

def test_upper(self):
self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
# 使用 loadTestsFromTestCase 获取当前脚本的用例类
test_case1 = unittest.TestLoader().loadTestsFromTestCase(MyTestCase)
# 使用 loadTestsFromModule 获取 loadTestsFromTestCaseDemo 脚本中的用例类
test_case2 = unittest.TestLoader().loadTestsFromModule(loadTestsFromTestCaseDemo)
# 创建suite并添加用例类
suite = unittest.TestSuite()
suite.addTests([test_case1, test_case2])
unittest.TextTestRunner(verbosity=2).run(suite)

上例中,​​loadTestsFromModule​​只要传入用例类所在的脚本名即可。

TestLoader.loadTestsFromName && TestLoader.loadTestsFromNames
​​​loadTestsFromTestCaseDemo.py​​​代码不变。
再来看​​​maMain.py​​:

import unittest
from loadTestsFromTestCaseDemo import loadTestsFromTestCaseDemo


class MyTestCase(unittest.TestCase):

def test_upper(self):
self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
# 使用 loadTestsFromName 获取当前脚本用例类的用例方法名称
test_case1 = unittest.TestLoader().loadTestsFromName(name='MyTestCase.test_upper', module=__import__(__name__))
# 使用 loadTestsFromNames 获取 loadTestsFromTestCaseDemo脚本中的LoadTestsFromTestCaseDemo1用例类的用例方法名
test_case2 = unittest.TestLoader().loadTestsFromNames(
names=['LoadTestsFromTestCaseDemo1.test_is_upper',
'LoadTestsFromTestCaseDemo1.test_is_lower'
],
module=loadTestsFromTestCaseDemo
)
# 创建suite并添加用例类
suite = unittest.TestSuite()
suite.addTests([test_case1, test_case2])
unittest.TextTestRunner(verbosity=2).run(suite)

切记,无论是​​loadTestsFromName​​​还是​​loadTestsFromNames​​,name参数都必须传递的是用例类下的方法名字,并且,方法名必须是全名。module参数就是脚本名字。

unittest.TestLoader().loadTestsFromNames(
name="ClassName.MethodName", # 类名点方法名
module=ModuleName # 脚本名
)

TestLoader.discover
首先,创建一些测试用例,注意,一定要知道各文件所在的位置。
​​​M:\tests\discover\test_list.py​​代码如下:

import unittest

class TextCaseList(unittest.TestCase):

def test_list_append(self):
l = []
l.append('a')
self.assertEqual(l, ['a']) # 判断 l 是否等于 ['a']

def test_list_remove(self):
l = ['a']
l.remove('a')
self.assertEqual(l, [])

创建了两个关于list的测试用例。
来看​​​M:\tests\discover\test_str.py​​的代码示例:

import unittest

class TextCaseStr(unittest.TestCase):

def test_str_index(self):
self.assertEqual('abc'.index('a'), 0)

def test_str_find(self):
self.assertEqual('abc'.find('a'), 0)

创建了两个关于str的测试用例。
来看​​​M:\tests\discover\son\test_dict.py​​的代码示例:

import unittest

class TextCaseDict(unittest.TestCase):

def test_dict_get(self):
d = {'a': 1}
self.assertEqual(d.get('a'), 1)

def test_dict_pop(self):
d = {'a': 1}
self.assertEqual(d.pop('a'), 1)

创建了两个关于dict的测试用例。
来看​​​M:\tests\test_tuple.py​​的代码示例:

import unittest

class TextCaseTuple(unittest.TestCase):

def test_tuple_count(self):
t = ('a', 'b')
self.assertEqual(t.count('a'), 1)

def test_tuple_index(self):
t = ('a', 'b')
self.assertEqual(t.index('a'), 0)

这样,在不同的目录中,新建了8个测试用例。
来研究一下discover怎么玩的。
discover部分无比重要,需要注意的地方有很多。要打起精神哦!
首先,来看discover的语法:

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)。

这里再补充一点。
我们知道,TestLoader类根据各种标准加载测试用例,并将它们返回给测试套件(suite)。但一般的,我们也可以不需要创建这个类实例(想要用某个类的方法,通常都是通过个该类的实例化对象调用)。unittest已经帮我们实例化好了TestLoader对象————defaultTestLoader,我们可以直接使用defaultTestLoader.discover。

discover = unittest.defaultTestLoader.discover(
start_dir=base_dir,
pattern='test*.py',
top_level_dir=base_dir
)
unittest.TextTestRunner(verbosity=2).run(discover)

最后,仔细品味示例吧:

import os
import unittest

class MyTestCase(unittest.TestCase):

def test_upper(self):
self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
base_dir = os.path.dirname(os.path.abspath(__name__)) # M:\tests
discover_dir = os.path.join(base_dir, 'discover') # M:\tests\discover
son_dir = os.path.join(discover_dir, 'son') # M:\tests\discover\son
print(base_dir, discover_dir, son_dir)
'''
# start_dir 和top_level_dir 的目录一致,获取该 start_dir 目录及子目录内的所有以 test 开头的 py 文件中的测试用例类
discover = unittest.defaultTestLoader.discover(start_dir=base_dir, pattern='test*.py', top_level_dir=base_dir)
unittest.TextTestRunner(verbosity=2).run(discover) # 8个用例被执行
'''
# start_dir 是 top_level_dir 的子目录,获取该 start_dir 目录及子目录内的所有以 test 开头的 py 文件中的测试用例类
discover = unittest.defaultTestLoader.discover(start_dir=discover_dir, pattern='test*.py', top_level_dir=base_dir)
unittest.TextTestRunner(verbosity=2).run(discover) # 6个用例被执行

# discover = unittest.TestLoader().discover(start_dir=base_dir)
# unittest.TextTestRunner(verbosity=2).run(discover)

在参考示例时,心里默念注意事项。

一探unittest.main

 

现在,makeSuite虽然很好用,但是依然不够,我们需要更加便捷和省事,一般情况下,我们更加倾向专注于编写测试用例,而后直接使用unittest执行即可,希望makeSuite这一步都能由unittest来完成,而不是我们自己来。
是的,懒惰既是美德!Python或者unittest做到了:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def test_add(self):
self.assertEqual(case_set.add(2, 3), 5)

def test_sub(self):
self.assertEqual(case_set.sub(10, 5), 2) # AssertionError: 5 != 2

def test_mul(self):
self.assertEqual(case_set.mul(10, 5), 50)

def test_div(self):
self.assertEqual(case_set.div(10, 5), 2)

if __name__ == '__main__':
unittest.main()

正如上例,我们只需要在用例类中将用例方法以​​test​​​开头,然后直接​​unittest.main()​​​就可以直接测试了。
我想通过前面的铺垫,这里也能大致的知道​​​unittest.main()​​在内部做了什么了。我们将在最后来剖析它背后的故事。现在还有一些重要的事情等着我们。

setUpClass && tearDownClass

 

在开始,我们学习了在测试某一个用例时,都会对应的执行三个方法:

  • setUp,开头一枪的那家伙,它负责该用例之前可能需要的一些准备,比如连接数据库。
  • runTest,执行用例逻辑,没的说,干活的长工。
  • tearDown,负责打扫战场,比如关闭数据库连接。

示例:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def test_add(self):
self.assertEqual(case_set.add(2, 3), 5)

def test_sub(self):
self.assertEqual(case_set.sub(10, 5), 5)

def setUp(self):
""" 如果myUnitTest中有我,我将在用例之前执行,无论我在myUnitTest的什么位置 """
print('敌军还有三十秒到达战场, 碾碎他们....')

def tearDown(self):
""" 如果myUnitTest中有我,我将在用例之后执行,无论我在myUnitTest的什么位置 """
print('ace .....')

if __name__ == '__main__':
unittest.main()

结果:

敌军还有三十秒到达战场, 碾碎他们....
True
打完收工,阿sir出来洗地了.....
.敌军还有三十秒到达战场, 碾碎他们....
False
打完收工,阿sir出来洗地了.....
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

由结果可以看到,有两个用例被执行并通过,并且,每一个用例执行前后都触发了setUp和tearDown方法执行。
但是,同志们,如果这是由1000甚至更多的用例组成的用例集,并且每一个用例都去操作数据,那么每个用例都会做连接/关闭数据库的操作。这就蛋疼了,就不能一次连接,所有用例都完事后,再关闭?这一下一下的......
是的,可以解决这个问题:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def test_add(self):
self.assertEqual(case_set.add(2, 3), 5)

def test_sub(self):
self.assertEqual(case_set.sub(10, 5), 5)

def setUp(self):
print('敌军还有三十秒到达战场, 碾碎他们....')

def tearDown(self):
print('打完收工,阿sir出来洗地了.....')

@classmethod
def setUpClass(cls):
print('在用例集开始执行,我去建立数据库连接......')

@classmethod
def tearDownClass(cls):
print('全军撤退, 我收工.......')

if __name__ == '__main__':
unittest.main()

结果:

在用例集开始执行,我去建立数据库连接......
敌军还有三十秒到达战场, 碾碎他们....
True
打完收工,阿sir出来洗地了.....
.敌军还有三十秒到达战场, 碾碎他们....
False
打完收工,阿sir出来洗地了.....
.全军撤退, 我收工.......

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

由结果可以看到,​​setUpClass​​​和​​tearDownClass​​这两个类方法完美的解决我们的问题,这让我们在某些情况下可以更加灵活的组织逻辑。

verbosity参数

 

verbosity
上述的断言结果虽然很清晰,但是还不够!我们可以控制错误输出的详细程度。

import unittest

class TestStringMethods(unittest.TestCase):

def test_assertFalse(self):
self.assertFalse('')

if __name__ == '__main__':
unittest.main(verbosity=1)

在执行​​unittest.main(verbosity=1)​​​时,可以通过​​verbosity​​​参数来控制错误信息的详细程度。
​​​verbosity=0​​:

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

​verbosity=1​​:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

​verbosity=2​​:

test_assertFalse (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

由结果可以总结,verbosity有3种的错误信息状态提示报告:

  • 0,静默模式,对于测试结果给予简单提示。
  • 1,默认模式,与静默模式类似,只是在每个成功的用例前面有个​​.​​​每个失败的用例前面有个​​F​​​,跳过的用例有个​​S​​。
  • 2,详细模式,测试结果会显示每个用例的所有相关的信息。

切记,只有​​0、1、2​​​三种状态。
默认的是1。
-v
除此之外,我们在终端执行时也可以输出详细报告:

M:\tests>python36 myMain.py -v
test_assertFalse (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

如上示例,使​​verbosity​​​参数保持默认,我们通过在终端加​​-v​​来输入详细报告信息。

除了​​-v​​,还可以有:

M:\tests>python36 myMain.py -p    # 等效于verbosity=0

什么都不加,就是​​verbosity=1​​。

跳过测试用例:skip

 

从Python3.1版本开始,unittest支持跳过单个测试方法甚至整个测试类。
也就是说,某些情况下,我们需要跳过指定的用例。
我们可以使用unittest提供的相关装饰器来完成:

decorators

description

@unittest.skip(reason)

无条件地跳过装饰测试用例。 理由应该描述为什么跳过测试用例。

@unittest.skipIf(condition, reason)

如果条件为真,则跳过修饰的测试用例。

@unittest.skipUnless(condition, reason)

除非条件为真,否则跳过修饰的测试用例。

@unittest.expectedFailure

将测试标记为预期的失败。如果测试失败,将被视为成功。如果测试用例通过,则认为是失败。

expection unittest.SkipTest(reason)

引发此异常以跳过测试测试用例。

示例:

import unittest

class TestCase01(unittest.TestCase):

def test_assertTrue(self):
self.assertTrue('')

@unittest.skip('no test') # 跳过该条用例
def test_assertFalse(self):
self.assertFalse('')

@unittest.skip('no test') # 跳过这个用例类
class TestCase02(unittest.TestCase):

def test_assertTrue(self):
self.assertTrue('')

def test_assertFalse(self):
self.assertFalse('')

if __name__ == '__main__':
unittest.main()

看结果:

M:\tests>python36 myMain.py
sFss
======================================================================
FAIL: test_assertTrue (__main__.TestCase01)
----------------------------------------------------------------------
Traceback (most recent call last):
File "demo0.py", line 27, in test_assertTrue
self.assertTrue('')
AssertionError: '' is not true

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1, skipped=3)

毋庸置疑,在结果中,总共4个用例,一个用例类被跳过,另一个用例类中跳过一个方法,那么就是执行4个用例,跳过3个

再探unittest.main

 

在解释器的​​Lib\unittest​​框架内,主要目录和文件,故事将会在这里展开。

**\Lib\unittest\
├─test\ # 目录
├─case.py
├─loader.py
├─main.py
├─mock.py
├─result.py
├─runner.py
├─signals.py
├─suite.py
├─util.py
├─__init__.py
└─__main__.py

现在,我们在脚本中执行这样一段代码:

import unittest

class TestStringMethods(unittest.TestCase):

def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')

def test_isupper(self):
self.assertTrue('FOO'.isupper())

if __name__ == '__main__':
unittest.main()

当我们在终端执行:

M:\tests>python36 myMain.py -v
test_isupper (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

unittest源码是这样执行的.........

在​​main.py​​​文件中。
main = TestProgram

class TestProgram(object):
# defaults for testing
module=None
verbosity = 1
failfast = catchbreak = buffer = progName = warnings = None
_discovery_parser = None

def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None, warnings=None, *, tb_locals=False):
print(argv) # ['myMain.py', '-v']
self.parseArgs(argv) # 检查参数
self.runTests() # 执行测试用例集
main = TestProgram

首先可以看到,​​main = TestProgram​​​,所以,​​unittest.main()​​​相当于​​unittest.TestProgram()​​​。类加括号是实例化的过程,所以,我们将目光集中在​​__init__​​​方法中,为实例化对象添加属性我们先略过,主要来看在这里都是执行了哪些方法。
可以看到主要做了两件事,​​​self.parseArgs(argv)​​​检查终端是否有参数传入,是有参数​​-v​​​的。完事执行​​self.runTests()​​​。
先来研究检查参数的​​​self.parseArgs​​方法做了什么?

main.py: TestProgram.parseArgs

class TestProgram(object):
def parseArgs(self, argv):
self.createTests()

​parseArgs​​​经过一系列的操作,我们来到该方法的最后一行,​​self.createTests()​​​,见名知意,这家伙是要创建用例集啊,看看具体怎么玩的。
main.py: TestProgram.createTests

class TestProgram(object):
def createTests(self):
# self.testNames: None
# self.module: <module '__main__' from 'myMain.py'>
if self.testNames is None:
self.test = self.testLoader.loadTestsFromModule(self.module)
else:
self.test = self.testLoader.loadTestsFromNames(self.testNames, self.module)

首先判断​​self.testNames​​​是不是为None,这个参数是​​TestProgram.__init__(defaultTest=None)​​​中的​​defaultTest​​​参数,我们并没有传参,所以是None,那么就执行if条件。在if条件中执行了​​self.testLoader.loadTestsFromModule(self.module)​​​方法,并传递了​​self.module​​​参数,该参数其实就是我们运行的脚本文件名。
我们看看这个​​​self.testLoader.loadTestsFromModule​​​方法做了什么。
loader.py: TestLoader.loadTestsFromModule
​​​loadTestsFromModule​​​方法位于unittest框架下的​​loader.py​​​的​​TestLoader​​类中。

class TestLoader(object):
""" 根据各种标准生成测试用例集 """

def loadTestsFromModule(self, module, *args, pattern=None, **kws):
"""返回给定模块中用例类(可能有多个用例类)中的用例 suite """

tests = []
# dir(module)获取 myMain.py中所有顶级属性,包括类名、函数名、变量名
# 循环判断每一个属性并判断是否是case.TestCase的派生类
for name in dir(module):
obj = getattr(module, name)
# 如果是case.TestCase的派生类,就添加到tests的列表中
# 但在添加之前,要做类型检查判断
if isinstance(obj, type) and issubclass(obj, case.TestCase):
tests.append(self.loadTestsFromTestCase(obj))
# module:myMain.py
# module中没有 load_tests,所以 load_tests为None
load_tests = getattr(module, 'load_tests', None)
tests = self.suiteClass(tests)
# 因为load_tests为None,所以if语句不会执行,
if load_tests is not None:
try:
return load_tests(self, tests, pattern)
except Exception as e:
error_case, error_message = _make_failed_load_tests(
module.__name__, e, self.suiteClass)
self.errors.append(error_message)
return error_case
# tests: <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<__main__.TestStringMethods testMethod=test_isupper>, <__main__.TestStringMethods testMethod=test_upper>]>]>
return tests # 用例集 suite

def loadTestsFromTestCase(self, testCaseClass):
"""返回testCaseClass中包含的所有测试用例的 suite"""
# testCaseClass:是myMain.py中的用例类名 <class '__main__.TestStringMethods'>
if issubclass(testCaseClass, suite.TestSuite):
raise TypeError("Test cases should not be derived from "
"TestSuite. Maybe you meant to derive from "
"TestCase?")
# 获取testCaseClass中的所有以prefix指定的用例名
testCaseNames = self.getTestCaseNames(testCaseClass)
# print(testCaseClass, testCaseNames) # <class '__main__.TestStringMethods'> ['test_isupper', 'test_upper']
# 很明显,咱们的脚本中没有runTest
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
testCaseNames = ['runTest']
# 这就很明显了 self.suiteClass(map(testCaseClass, testCaseNames)) 在生成用例集的suite
loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
# loaded_suite:<unittest.suite.TestSuite tests=[<__main__.TestStringMethods testMethod=test_isupper>, <__main__.TestStringMethods testMethod=test_upper>]>
# loaded_suite.countTestCases(): 2
return loaded_suite # 返回用例集 suite

def getTestCaseNames(self, testCaseClass):
"""
返回在testCaseClass中找到的方法名的排序序列
"""

# self.testMethodPrefix:test
def isTestMethod(attrname, testCaseClass=testCaseClass,
prefix=self.testMethodPrefix):
return attrname.startswith(prefix) and \
callable(getattr(testCaseClass, attrname))

testFnNames = list(filter(isTestMethod, dir(testCaseClass)))
if self.sortTestMethodsUsing:
testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
return testFnNames # ['test_isupper', 'test_upper']

​loader.py​​​中的​​TestLoader​​中一共做了三件事:

  • 由​​main.py: TestProgram.createTests​​​方法触发了​​loader.py: TestLoader.loadTestsFromModule​​方法执行,在这个方法中,首先循环判断取出测试脚本中的所有的用例类。
  • 然后在循环判断中,如果判断测试脚本中的类是​​case.TestCase​​​的派生类,就调用​​loader.py: TestLoader.loadTestsFromTestCase​​​方法调用​​loader.py: TestLoader.getTestCaseNames​​​并将用例类传递进去,该方法获取到传过来的用例类名,然后去这个用例类中去找所有prefix开头的用例,然后以列表的形式返回给​​loader.py: TestLoader.loadTestsFromTestCase​​方法。
  • ​loader.py: TestLoader.loadTestsFromTestCase​​方法拿到用例列表后,生成用例集suite并返回调用者。

程序在​​loader.py​​​执行完毕,回到​​main.py: TestProgram.createTests​​​中。
​​​main.py: TestProgram.createTests​​​成功完成任务,生成了用例集 suite。程序再次回到了调用​​main.py: TestProgram.createTests​​​的方法中——
​​​main.py: TestProgram.parseArgs​​​,然后​​main.py: TestProgram.parseArgs​​​方法也执行完毕。程序继续回到调用处——​​main.py: TestProgram.__init__​​​方法中。
此时,创建用例集的suite完成。
程序由此继续往下执行。
main.py: TestProgram.runTests
有了用例集就要执行了,往下看。

class TestProgram(object):
def runTests(self):
# 实例化时没有传参,所以 self.catchbreak: None
if self.catchbreak:
installHandler()
# self.testRunner同样没有传参,为None
if self.testRunner is None:
# runner.TextTestRunner是runner.py中的TextTestRunner对象
self.testRunner = runner.TextTestRunner

if isinstance(self.testRunner, type):
try:
try:
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
warnings=self.warnings,
tb_locals=self.tb_locals)
except TypeError:
# didn't accept the tb_locals argument
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
warnings=self.warnings)
except TypeError:
# didn't accept the verbosity, buffer or failfast arguments
testRunner = self.testRunner()
else:
# it is assumed to be a TestRunner instance
testRunner = self.testRunner
# 实例化runner.py中的TextTestRunner类得到testRunner对象
# testRunner.run(self.test)依次执行每一个用例
# 将结果收集到self.result中
# self.test:<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<__main__.TestStringMethods testMethod=test_isupper>, <__main__.TestStringMethods testMethod=test_upper>]>]>

self.result = testRunner.run(self.test)
if self.exit:
# self.result.wasSuccessful(): <bound method TestResult.wasSuccessful of <unittest.runner.TextTestResult run=2 errors
=0 failures=0>>

sys.exit(not self.result.wasSuccessful())

执行用例没啥好说的,调用了​​runner.py: TextTestRunner.run​​方法依次执行每个用例并收集结果。

runner.py: TextTestRunner.run

class TextTestResult(result.TestResult):
""" 一个测试结果类,它可以将格式化的文本结果打印到流中 """
class TextTestRunner(object):
""" 以文本形式显示结果的测试运行器 """
def __init__(self, stream=None, descriptions=True, verbosity=1,
failfast=False, buffer=False, resultclass=None, warnings=None,
*, tb_locals=False):
""" 构造一个TextTestRunner. """
# sys.stderr: <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>
# sys.stderr将结果输出到屏幕
if stream is None:
stream = sys.stderr
self.stream = _WritelnDecorator(stream) # _WritelnDecorator:文件类的装饰对象
self.descriptions = descriptions
self.verbosity = verbosity
self.failfast = failfast
self.buffer = buffer
self.tb_locals = tb_locals
self.warnings = warnings
if resultclass is not None:
# self.resultclass: TextTestResult
self.resultclass = resultclass

def _makeResult(self):
# print(self.stream, self.descriptions, self.verbosity) # <unittest.runner._WritelnDecorator object at 0x0373D690> True 2
# 返回 TextTestResult 实例化对象
return self.resultclass(self.stream, self.descriptions, self.verbosity)

def run(self, test):
"运行给定的测试用例或测试套件"
# result: TextTestResult 实例化对象
result = self._makeResult()
registerResult(result)
'''
failfast是 TextTestRunner 的一个属性,缺省为False
作用: 如果failfast为True,一旦测试集中有测试案例failed或发生error立即终止当前整个测试执行,跳过剩下所有测试案例,也就是实现“短路测试”
'''
result.failfast = self.failfast # self.failfast: False
result.buffer = self.buffer
result.tb_locals = self.tb_locals
with warnings.catch_warnings():
if self.warnings:
# if self.warnings is set, use it to filter all the warnings
warnings.simplefilter(self.warnings)
# if the filter is 'default' or 'always', special-case the
# warnings from the deprecated unittest methods to show them
# no more than once per module, because they can be fairly
# noisy. The -Wd and -Wa flags can be used to bypass this
# only when self.warnings is None.
if self.warnings in ['default', 'always']:
warnings.filterwarnings('module',
category=DeprecationWarning,
message=r'Please use assert\w+ instead.')
startTime = time.time()
# result: TextTestResult
# TextTestResult中并没有 startTestRun,但是父类的 TestResult 中有
startTestRun = getattr(result, 'startTestRun', None)
if startTestRun is not None:
# 执行TestResult的startTestRun
startTestRun()
try:
# BaseTestSuite执行了 __call__ 方法,test加括号等于执行了 BaseTestSuite 的 run 方法
test(result)
finally:
# 用例执行完毕,触发 TestResult 的 stopTestRun 方法
stopTestRun = getattr(result, 'stopTestRun', None)
if stopTestRun is not None:
stopTestRun()
stopTime = time.time()
timeTaken = stopTime - startTime
result.printErrors()
if hasattr(result, 'separator2'):
self.stream.writeln(result.separator2)
run = result.testsRun
self.stream.writeln("Ran %d test%s in %.3fs" %
(run, run != 1 and "s" or "", timeTaken))
self.stream.writeln()

expectedFails = unexpectedSuccesses = skipped = 0
try:
results = map(len, (result.expectedFailures,
result.unexpectedSuccesses,
result.skipped))
except AttributeError:
pass
else:
expectedFails, unexpectedSuccesses, skipped = results

infos = []
if not result.wasSuccessful():
self.stream.write("FAILED")
failed, errored = len(result.failures), len(result.errors)
if failed:
infos.append("failures=%d" % failed)
if errored:
infos.append("errors=%d" % errored)
else:
self.stream.write("OK")
if skipped:
infos.append("skipped=%d" % skipped)
if expectedFails:
infos.append("expected failures=%d" % expectedFails)
if unexpectedSuccesses:
infos.append("unexpected successes=%d" % unexpectedSuccesses)
if infos:
self.stream.writeln(" (%s)" % (", ".join(infos),))
else:
self.stream.write("\n")
return result

总结:

  • 收集用例。
  • 根据用例生成测试集。
  • 运行测试集。

自定义删除用例方法

 

我们之前学习unittest.makeSuite时,学过两个添加用例的方法,但是我讲过删除用的方法了吗?并没有!现在,我们已经剖析了源码,知道了添加用例是​​addTest​​​和​​addTests​​​干的。
suite.py: BaseTestSuite:

class BaseTestSuite(object):

def addTest(self, test):
# sanity checks
if not callable(test):
raise TypeError("{} is not callable".format(repr(test)))
if isinstance(test, type) and issubclass(test,
(case.TestCase, TestSuite)):
raise TypeError("TestCases and TestSuites must be instantiated "
"before passing them to addTest()")
self._tests.append(test)

def addTests(self, tests):
if isinstance(tests, str):
raise TypeError("tests must be an iterable of tests, not a string")
for test in tests:
self.addTest(test)

可以看到,​​addTest​​​是一个一个添加,而​​addTests​​​则是for循环调用​​addTest​​​添加,本质上一样的。
让我们将目光聚集到​​​addTest​​​中,可以看到使用的是​​self._test.append(test)​​。现在,我们的删除方法也有了——把添加方法复制一份,改几个字即可:

class BaseTestSuite(object):

def addTest(self, test):
# sanity checks
if not callable(test):
raise TypeError("{} is not callable".format(repr(test)))
if isinstance(test, type) and issubclass(test,
(case.TestCase, TestSuite)):
raise TypeError("TestCases and TestSuites must be instantiated "
"before passing them to addTest()")
self._tests.append(test)

def removeTest(self, test):
# sanity checks
if not callable(test):
raise TypeError("{} is not callable".format(repr(test)))
if isinstance(test, type) and issubclass(test,
(case.TestCase, TestSuite)):
raise TypeError("TestCases and TestSuites must be instantiated "
"before passing them to addTest()")
self._tests.remove(test)

没错,你没看错,就是把​​addTest​​​复制一份,方法名改为​​removeTest​​​,完事把​​self._tests.append(test)​​​改为​​self._tests.remove(test)​​就行了。

调用也类似:

import unittest


class TestStringMethods(unittest.TestCase):

def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')

def test_isupper(self):
self.assertTrue('FOO'.isupper())

if __name__ == '__main__':
case = TestStringMethods('test_upper')
suite = unittest.TestSuite()
suite.addTest(case) # suite中有一个test_upper用例
print(suite.countTestCases()) # 1
suite.removeTest(case) # 删除掉它
print(suite.countTestCases()) # 0

将执行结果输出到文件

 

我们尝试着讲用例执行结果输出到文件中。

import unittest
class TestStringMethods(unittest.TestCase):

def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')

def test_isupper(self):
self.assertTrue('FOO'.isupper())

if __name__ == '__main__':
f = open(r'M:\tests\t1.txt', 'w', encoding='utf-8')
suite = unittest.makeSuite(TestStringMethods)
unittest.TextTestRunner(stream=f).run(suite)

生成用例报告

 

如上小节中,虽然能将结果输出到某个文件中,但更多的是根据模板生成报告,这里就来研究一下,如何生成模板报告。

参见:​​javascript:void(0)​​

发送测试报告邮件

 

参见:​​javascript:void(0)​​

unittest.mock

 

参见:​​javascript:void(0)​​

小结:在unittest中,我们需要掌握的几个类:

  • unittest.TestCase:所有测试用例的基类,给定一个测试用例方法的名字,就会返回一个测试用例实例。
  • unittest.TestSuite:组织测试用例的用例集,支持测试用例的添加和删除。
  • unittest.TextTestRunner:测试用例的执行,其中Text是以文本的形式显示测试结果,测试结果会保存到TextTestResult中。
  • unittest.TextTestResult:保存测试用例信息,包括运行了多少个测试用例,成功了多少,失败了多少等信息。
  • unittest.TestLoader:加载TestCase到TESTSuite中。
  • unittest.defaultTestLoader:等于​​unittest.TestLoader()​​。
  • unittest.TestProgram:TestProgram类名被赋值给了main变量,然后通过unittest.main()的形式调用。




About

 

unittest是Python内置的单元测试框架(模块),不仅可以完成单元测试,也适用于web自动化测试中。

unittest提供了丰富的断言方法,判断测试用例是否通过,然后生成测试结果报告。

必要的准备与注意事项

 

首先,我们准备这样一个目录:

M:\tests\  # 我的是M盘的tests目录,所有操作都在tests目录内完成
├─discover
│ ├─son
│ │ ├─test_dict.py
│ │ └─__init__.py
│ ├─test_list.py
│ ├─test_str.py
│ └─__init__.py
├─loadTestsFromTestCaseDemo
│ └─loadTestsFromTestCaseDemo.py
├─case_set.py
├─myMain.py # 代码演示文件,所有演示脚本文件
├─test_tuple.py
└─__init__.py

如果你跟我的流程走, 请务必建立和理解这样的一个目录,目前这些文件都是空的,后续会一一建立,各目录内的​​__init__.py​​也必须建立,虽然它是空的,但是它无比重要,因为它标明它所在目录是Python的包。

​case_set.py​​有4个函数,分别计算加减乘除,并且代码不变:

"""
用例集
"""


def add(x, y):
""" 两数相加 """
return x + y


def sub(x, y):
""" 两数相减 """
return x - y


def mul(x, y):
""" 两数相乘 """
return x * y


def div(x, y):
""" 两数相除 """
return x / y


if __name__ == '__main__':
print(div(10, 5))
print(div(10, 0))

上述4个函数将成为我们的测试用例。

另外,示例演示环境是:

python3.6 + windows10 + pycharm2018.1

注意!注意!!注意!!!

如果你对pycharm的使用烂熟于心,那么在运行接下来的示例时,请不要右键运行或者点击运行按钮执行脚本,而是通过​​Terminal​​或者终端执行脚本,因为pycharm的集成环境会影响测试结果。

unittest简单上手

 

runTest

import unittest  # 导入unittest框架
import case_set # 导入用例集

class myUnitTest(unittest.TestCase):

def setUp(self):
""" 用例初始化 """
print("用例初始化 setup")
def runTest(self):
""" 执行用例 """
print(case_set.add(2, 3) == 5)
def tearDown(self):
""" 用例执行完,收尾 """
print("用例执行完毕,收尾")
if __name__ == '__main__':
demo = myUnitTest()
demo.run() # 固定的调用方法run

执行结果:

Ran 1 test in 0.002s

OK
用例初始化 setup
True
用例执行完毕,收尾

由结果可以看到,1个用例在多少时间内执行完毕,并且用例执行通过。

用例的执行流程是:

  • setUp先开第一枪,处理一些初始化操作。
  • 接着runTest执行用例,用例返回True。
  • 最后,tearDown打扫战场!

在每个用例执行时,setUp和tearDown都会执行。

注意:

  • myUnitTest类名可以自定义,但是必须继承​​unittest.TestCase​​。
  • 示例中的setUp和tearDown方法名是固定的,但如果,我们测试用例时,没有初始化和收尾的工作,setUp和tearDown方法可以省略不写。

至于runTest方法名叫什么,取决于在实例化myUnitTest类时,是否传参,我们来看​​unittest.TestCase​​​类的​​__init__​​方法和run方法做了什么:

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)

可以看到,在实例化的时候,其实有个​​methodName​​​默认参数,正好也叫runTest。而在实例化后,实例化对象调用run方法的时候,反射了那个​​methodName​​值,然后用例正常执行了。

所以,runTest方法名可以自定义:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
print(case_set.add(2, 3) == 5)

if __name__ == '__main__':
demo = myUnitTest(methodName='add_test')
demo.run()

执行多个用例

那么,如果要执行多个用例怎么办?

import unittest
import case_set

class myUnitTestAdd(unittest.TestCase):

def runTest(self):
""" 执行用例 """
print(case_set.add(2, 3) == 5)

class myUnitTestSub(unittest.TestCase):

def runTest(self):
""" 执行用例 """
print(case_set.sub(2, 3) == 5) # 用例结果不符合预期

if __name__ == '__main__':
demo1 = myUnitTestAdd()
demo2 = myUnitTestSub()
demo1.run()
demo2.run()

上面的示例,也可以写成:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
print(case_set.add(2, 3) == 5)

def sub_test(self):
""" 执行用例"""
print(case_set.sub(10, 5) == 2)

if __name__ == '__main__':
demo1 = myUnitTest('add_test')
demo2 = myUnitTest('sub_test')
demo1.run()
demo2.run()

如上方式,每个用例都要实例化一次,虽然可以执行多个用例,但是这么写实在是太low了,反倒没有之前测试除法用例来的简单。

另外,用print打印也不符合真实的测试环境。

我们先来解决print的问题。

使用unittest提供的断言

 

来看看unittest为我们提供了哪些断言方法吧!

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

下表列出了最常用的方法:

Method

Checks that

description

New in

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

示例:

import unittest


class TestStringMethods(unittest.TestCase):

def test_assertEqual(self):
self.assertEqual(1, 2, msg='1 != 2') # AssertionError: 1 != 2 : 1 != 2

def test_assertTrue(self):
self.assertTrue('')

def test_assertFalse(self):
self.assertFalse('')


if __name__ == '__main__':
unittest.main()

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

结果示例:

F.F
======================================================================
FAIL: test_assertEqual (__main__.TestStringMethods)
----------------------------------------------------------------------
Traceback (most recent call last):
File "myMain.py", line 251, in test_assertEqual
self.assertEqual(1, 2, msg='1 != 2') # AssertionError: 1 != 2 : 1 != 2
AssertionError: 1 != 2 : 1 != 2

======================================================================
FAIL: test_assertTrue (__main__.TestStringMethods)
----------------------------------------------------------------------
Traceback (most recent call last):
File "myMain.py", line 254, in test_assertTrue
self.assertTrue('')
AssertionError: '' is not true

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=2)

结果中,​​F.F​​​表示,如果用例通过返回​​.​​​,失败返回​​F​​​,所以结果告诉我们执行了3个用例,成功1个,失败两个​​FAILED (failures=2)​​​,​​AssertionError​​是错误信息。

unittest.TestSuite

 

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

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

当添加完所有用例后,该测试套件将被交给测试执行(运行)器,如TextTestRunner,该执行器会按照用例的添加顺序执行各用例,并聚合结果。

TestSuite有效的解决了:

  • 因为是顺序执行,当多个用例组成一个链式测试操作时,谁先谁后的问题就不存在了。
  • 有效地将多个用例组织到一起进行集中测试,解决了之前一个一个测试的问题。

suite_obj.addTest(self, test)

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
""" 执行用例"""
self.assertEqual(case_set.sub(10, 5), 5)

def create_suite():
""" 创建用例集 """
# 拿到两个用例对象
add = myUnitTest('add_test')
sub = myUnitTest('sub_test')
# 实例化suite对象
suite_obj = unittest.TestSuite()
# 添加用例
suite_obj.addTest(add)
suite_obj.addTest(sub)
return suite_obj

if __name__ == '__main__':
suite = create_suite()
# 可以查看suite中的用例数量
# print(suite.countTestCases()) # 2
# 拿到执行器对象
runner = unittest.TextTestRunner()
# 你想用执行器执行谁?就把它传进去
runner.run(suite)

代码注释已经说得很明白了,只需要记住可以通过​​suite.countTestCases()​​方法获取suite中用例的数量。

suite_obj.addTests(self, tests)

一个一个往suite中添加用例比较麻烦,所以,再来个简单的:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
""" 执行用例"""
self.assertEqual(case_set.sub(10, 5), 5)


def create_suite():
""" 创建用例集 """
'''
# 拿到两个用例对象
add = myUnitTest('add_test')
sub = myUnitTest('sub_test')
# 实例化suite对象
suite_obj = unittest.TestSuite()
# 添加用例
suite_obj.addTests([add, sub])
'''
# 上面的代码也可以这么写
map_obj = map(myUnitTest, ['add_test', 'sub_test'])
suite_obj = unittest.TestSuite()
suite_obj.addTests(map_obj)
return suite_obj

if __name__ == '__main__':

suite = create_suite()
# 可以查看suite中的用例数量
# print(suite.countTestCases()) # 2
# 拿到执行器对象
runner = unittest.TextTestRunner()
# 你想用执行器执行谁?就把它传进去
runner.run(suite)

实例化时添加用例

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
""" 执行用例"""
self.assertEqual(case_set.sub(10, 5), 5)

def create_suite():
""" 创建用例集 """
map_obj = map(myUnitTest, ['add_test', 'sub_test'])
suite_obj = unittest.TestSuite(tests=map_obj)
return suite_obj

if __name__ == '__main__':
suite = create_suite()
runner = unittest.TextTestRunner()
runner.run(suite)

怎么玩的呢?其实我们在实例化时做了添加用例的操作,以下示例演示了实例化的过程:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
""" 执行用例 """
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
""" 执行用例"""
self.assertEqual(case_set.sub(10, 5), 5)

class myUnitTestSuite(unittest.TestSuite):
def __init__(self):
# 当实例化suite对象时,传递用例
map_obj = map(myUnitTest, ['add_test', 'sub_test'])
# 调用父类的 __init__ 方法
super().__init__(tests=map_obj)

if __name__ == '__main__':
suite_obj = myUnitTestSuite()
runner = unittest.TextTestRunner()
runner.run(suite_obj)

虽然在一定程度上,我们优化了代码,但是还不够,因为,我们还需要手动的将用例添加到suite的中。接下来,我们来学习,如何自动添加。

unittest.makeSuite

 

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def add_test(self):
self.assertEqual(case_set.add(2, 3), 5)

def sub_test(self):
self.assertEqual(case_set.sub(10, 5), 2)

def test_mul(self):
self.assertEqual(case_set.mul(10, 5), 50)

def test_div(self):
self.assertEqual(case_set.div(10, 5), 2)

def create_suite():
""" 创建用例集 """
suite_obj = unittest.makeSuite(testCaseClass=myUnitTest, prefix='test')
return suite_obj

if __name__ == '__main__':
suite_obj = create_suite()
print(suite_obj.countTestCases()) # 2
runner = unittest.TextTestRunner()
runner.run(suite_obj)

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

再次强调,prefix参数默认读取以test开头的用例,也可以自己指定:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def my_add_test(self):
self.assertEqual(case_set.add(2, 3), 5)

def my_sub_test(self):
self.assertEqual(case_set.sub(10, 5), 2) # AssertionError: 5 != 2

def test_mul(self):
self.assertEqual(case_set.mul(10, 5), 50)

def test_div(self):
self.assertEqual(case_set.div(10, 5), 2)

def create_suite():
""" 创建用例集 """
suite_obj = unittest.makeSuite(myUnitTest, prefix='my')
return suite_obj

if __name__ == '__main__':
suite_obj = create_suite()
print(suite_obj.countTestCases()) # 2
runner = unittest.TextTestRunner()
runner.run(suite_obj)

如上例示例,读取myUnitTest类中所有以​​my​​开头的用例方法。但建议还是按照人家默认的test就好了。

除此之外,这都9102年了, 车车都是手自一体的,咱们除了能玩自动添加,也能手动的将指定的用例添加到suite中:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def my_add_test(self):
self.assertEqual(case_set.add(2, 3), 5)

def my_sub_test(self):
self.assertEqual(case_set.sub(10, 5), 2) # AssertionError: 5 != 2

def test_mul(self):
self.assertEqual(case_set.mul(10, 5), 50)

def test_div(self):
self.assertEqual(case_set.div(10, 5), 2)

def create_suite():
""" 创建用例集 """
suite_obj = unittest.makeSuite(myUnitTest, prefix='my')
suite_obj.addTests(map(myUnitTest, ['test_mul', 'test_div']))
return suite_obj

if __name__ == '__main__':
suite_obj = create_suite()
print(suite_obj.countTestCases()) # 4
runner = unittest.TextTestRunner()
runner.run(suite_obj)

上例,使用makeSuite自动添加所有以​​my​​开头的用例,然后又使用addTests添加两个用例。

unittest.TestLoader

 

到目前为止,我们所有的用例方法都封装在一个用例类中,但是有的时候,我们会根据不同的功能编写不同的测试用例文件,甚至是存放在不同的目录内。

这个时候在用addTest添加就非常的麻烦了。
unittest提供了TestLoader类来解决这个问题。先看提供了哪些方法:

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

执行脚本文件为​​myMain.py​​​,目录结构,参见开头的目录结构示例。
TestLoader.loadTestsFromTestCase

首先,​​loadTestsFromTestCaseDemo.py​​代码如下:

import unittest

class LoadTestsFromTestCaseDemo(unittest.TestCase):

def test_is_upper(self):
self.assertTrue('FOO'.isupper())

def test_is_lower(self):
self.assertTrue('foo'.islower())

​LoadTestsFromTestCaseDemo​​类中有两个测试用例。

import unittest
from loadTestsFromTestCaseDemo.loadTestsFromTestCaseDemo import LoadTestsFromTestCaseDemo

class MyTestCase(unittest.TestCase):

def test_upper(self):
self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
# 使用loadTestsFromTestCase获取当前脚本和loadTestsFromTestCaseDemo脚本中的用例类
test_case1 = unittest.TestLoader().loadTestsFromTestCase(MyTestCase)
test_case2 = unittest.TestLoader().loadTestsFromTestCase(LoadTestsFromTestCaseDemo)
# 创建suite并添加用例类
suite = unittest.TestSuite()
suite.addTests([test_case1, test_case2])
unittest.TextTestRunner(verbosity=2).run(suite)

上例中,​​loadTestsFromTestCase​​需要传入用例类的类名。无所谓这个用例类所处的目录或者文件。

TestLoader.loadTestsFromModule

​loadTestsFromTestCaseDemo.py​​代码稍微有些变动:

import unittest

class LoadTestsFromTestCaseDemo1(unittest.TestCase):

def test_is_upper(self):
self.assertTrue('FOO'.isupper())

def test_is_lower(self):
self.assertTrue('foo'.islower())

class LoadTestsFromTestCaseDemo2(unittest.TestCase):

def test_startswith(self):
self.assertTrue('FOO'.startswith('F'))

def test_endswith(self):
self.assertTrue('foo'.endswith('o'))

再来看​​maMain.py​​:

import unittest
from loadTestsFromTestCaseDemo import loadTestsFromTestCaseDemo

class MyTestCase(unittest.TestCase):

def test_upper(self):
self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
# 使用 loadTestsFromTestCase 获取当前脚本的用例类
test_case1 = unittest.TestLoader().loadTestsFromTestCase(MyTestCase)
# 使用 loadTestsFromModule 获取 loadTestsFromTestCaseDemo 脚本中的用例类
test_case2 = unittest.TestLoader().loadTestsFromModule(loadTestsFromTestCaseDemo)
# 创建suite并添加用例类
suite = unittest.TestSuite()
suite.addTests([test_case1, test_case2])
unittest.TextTestRunner(verbosity=2).run(suite)

上例中,​​loadTestsFromModule​​只要传入用例类所在的脚本名即可。

TestLoader.loadTestsFromName && TestLoader.loadTestsFromNames
​​​loadTestsFromTestCaseDemo.py​​​代码不变。
再来看​​​maMain.py​​:

import unittest
from loadTestsFromTestCaseDemo import loadTestsFromTestCaseDemo


class MyTestCase(unittest.TestCase):

def test_upper(self):
self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
# 使用 loadTestsFromName 获取当前脚本用例类的用例方法名称
test_case1 = unittest.TestLoader().loadTestsFromName(name='MyTestCase.test_upper', module=__import__(__name__))
# 使用 loadTestsFromNames 获取 loadTestsFromTestCaseDemo脚本中的LoadTestsFromTestCaseDemo1用例类的用例方法名
test_case2 = unittest.TestLoader().loadTestsFromNames(
names=['LoadTestsFromTestCaseDemo1.test_is_upper',
'LoadTestsFromTestCaseDemo1.test_is_lower'
],
module=loadTestsFromTestCaseDemo
)
# 创建suite并添加用例类
suite = unittest.TestSuite()
suite.addTests([test_case1, test_case2])
unittest.TextTestRunner(verbosity=2).run(suite)

切记,无论是​​loadTestsFromName​​​还是​​loadTestsFromNames​​,name参数都必须传递的是用例类下的方法名字,并且,方法名必须是全名。module参数就是脚本名字。

unittest.TestLoader().loadTestsFromNames(
name="ClassName.MethodName", # 类名点方法名
module=ModuleName # 脚本名
)

TestLoader.discover
首先,创建一些测试用例,注意,一定要知道各文件所在的位置。
​​​M:\tests\discover\test_list.py​​代码如下:

import unittest

class TextCaseList(unittest.TestCase):

def test_list_append(self):
l = []
l.append('a')
self.assertEqual(l, ['a']) # 判断 l 是否等于 ['a']

def test_list_remove(self):
l = ['a']
l.remove('a')
self.assertEqual(l, [])

创建了两个关于list的测试用例。
来看​​​M:\tests\discover\test_str.py​​的代码示例:

import unittest

class TextCaseStr(unittest.TestCase):

def test_str_index(self):
self.assertEqual('abc'.index('a'), 0)

def test_str_find(self):
self.assertEqual('abc'.find('a'), 0)

创建了两个关于str的测试用例。
来看​​​M:\tests\discover\son\test_dict.py​​的代码示例:

import unittest

class TextCaseDict(unittest.TestCase):

def test_dict_get(self):
d = {'a': 1}
self.assertEqual(d.get('a'), 1)

def test_dict_pop(self):
d = {'a': 1}
self.assertEqual(d.pop('a'), 1)

创建了两个关于dict的测试用例。
来看​​​M:\tests\test_tuple.py​​的代码示例:

import unittest

class TextCaseTuple(unittest.TestCase):

def test_tuple_count(self):
t = ('a', 'b')
self.assertEqual(t.count('a'), 1)

def test_tuple_index(self):
t = ('a', 'b')
self.assertEqual(t.index('a'), 0)

这样,在不同的目录中,新建了8个测试用例。
来研究一下discover怎么玩的。
discover部分无比重要,需要注意的地方有很多。要打起精神哦!
首先,来看discover的语法:

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)。

这里再补充一点。
我们知道,TestLoader类根据各种标准加载测试用例,并将它们返回给测试套件(suite)。但一般的,我们也可以不需要创建这个类实例(想要用某个类的方法,通常都是通过个该类的实例化对象调用)。unittest已经帮我们实例化好了TestLoader对象————defaultTestLoader,我们可以直接使用defaultTestLoader.discover。

discover = unittest.defaultTestLoader.discover(
start_dir=base_dir,
pattern='test*.py',
top_level_dir=base_dir
)
unittest.TextTestRunner(verbosity=2).run(discover)

最后,仔细品味示例吧:

import os
import unittest

class MyTestCase(unittest.TestCase):

def test_upper(self):
self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
base_dir = os.path.dirname(os.path.abspath(__name__)) # M:\tests
discover_dir = os.path.join(base_dir, 'discover') # M:\tests\discover
son_dir = os.path.join(discover_dir, 'son') # M:\tests\discover\son
print(base_dir, discover_dir, son_dir)
'''
# start_dir 和top_level_dir 的目录一致,获取该 start_dir 目录及子目录内的所有以 test 开头的 py 文件中的测试用例类
discover = unittest.defaultTestLoader.discover(start_dir=base_dir, pattern='test*.py', top_level_dir=base_dir)
unittest.TextTestRunner(verbosity=2).run(discover) # 8个用例被执行
'''
# start_dir 是 top_level_dir 的子目录,获取该 start_dir 目录及子目录内的所有以 test 开头的 py 文件中的测试用例类
discover = unittest.defaultTestLoader.discover(start_dir=discover_dir, pattern='test*.py', top_level_dir=base_dir)
unittest.TextTestRunner(verbosity=2).run(discover) # 6个用例被执行

# discover = unittest.TestLoader().discover(start_dir=base_dir)
# unittest.TextTestRunner(verbosity=2).run(discover)

在参考示例时,心里默念注意事项。

一探unittest.main

 

现在,makeSuite虽然很好用,但是依然不够,我们需要更加便捷和省事,一般情况下,我们更加倾向专注于编写测试用例,而后直接使用unittest执行即可,希望makeSuite这一步都能由unittest来完成,而不是我们自己来。
是的,懒惰既是美德!Python或者unittest做到了:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def test_add(self):
self.assertEqual(case_set.add(2, 3), 5)

def test_sub(self):
self.assertEqual(case_set.sub(10, 5), 2) # AssertionError: 5 != 2

def test_mul(self):
self.assertEqual(case_set.mul(10, 5), 50)

def test_div(self):
self.assertEqual(case_set.div(10, 5), 2)

if __name__ == '__main__':
unittest.main()

正如上例,我们只需要在用例类中将用例方法以​​test​​​开头,然后直接​​unittest.main()​​​就可以直接测试了。
我想通过前面的铺垫,这里也能大致的知道​​​unittest.main()​​在内部做了什么了。我们将在最后来剖析它背后的故事。现在还有一些重要的事情等着我们。

setUpClass && tearDownClass

 

在开始,我们学习了在测试某一个用例时,都会对应的执行三个方法:

  • setUp,开头一枪的那家伙,它负责该用例之前可能需要的一些准备,比如连接数据库。
  • runTest,执行用例逻辑,没的说,干活的长工。
  • tearDown,负责打扫战场,比如关闭数据库连接。

示例:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def test_add(self):
self.assertEqual(case_set.add(2, 3), 5)

def test_sub(self):
self.assertEqual(case_set.sub(10, 5), 5)

def setUp(self):
""" 如果myUnitTest中有我,我将在用例之前执行,无论我在myUnitTest的什么位置 """
print('敌军还有三十秒到达战场, 碾碎他们....')

def tearDown(self):
""" 如果myUnitTest中有我,我将在用例之后执行,无论我在myUnitTest的什么位置 """
print('ace .....')

if __name__ == '__main__':
unittest.main()

结果:

敌军还有三十秒到达战场, 碾碎他们....
True
打完收工,阿sir出来洗地了.....
.敌军还有三十秒到达战场, 碾碎他们....
False
打完收工,阿sir出来洗地了.....
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

由结果可以看到,有两个用例被执行并通过,并且,每一个用例执行前后都触发了setUp和tearDown方法执行。
但是,同志们,如果这是由1000甚至更多的用例组成的用例集,并且每一个用例都去操作数据,那么每个用例都会做连接/关闭数据库的操作。这就蛋疼了,就不能一次连接,所有用例都完事后,再关闭?这一下一下的......
是的,可以解决这个问题:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

def test_add(self):
self.assertEqual(case_set.add(2, 3), 5)

def test_sub(self):
self.assertEqual(case_set.sub(10, 5), 5)

def setUp(self):
print('敌军还有三十秒到达战场, 碾碎他们....')

def tearDown(self):
print('打完收工,阿sir出来洗地了.....')

@classmethod
def setUpClass(cls):
print('在用例集开始执行,我去建立数据库连接......')

@classmethod
def tearDownClass(cls):
print('全军撤退, 我收工.......')

if __name__ == '__main__':
unittest.main()

结果:

在用例集开始执行,我去建立数据库连接......
敌军还有三十秒到达战场, 碾碎他们....
True
打完收工,阿sir出来洗地了.....
.敌军还有三十秒到达战场, 碾碎他们....
False
打完收工,阿sir出来洗地了.....
.全军撤退, 我收工.......

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

由结果可以看到,​​setUpClass​​​和​​tearDownClass​​这两个类方法完美的解决我们的问题,这让我们在某些情况下可以更加灵活的组织逻辑。

verbosity参数

 

verbosity
上述的断言结果虽然很清晰,但是还不够!我们可以控制错误输出的详细程度。

import unittest

class TestStringMethods(unittest.TestCase):

def test_assertFalse(self):
self.assertFalse('')

if __name__ == '__main__':
unittest.main(verbosity=1)

在执行​​unittest.main(verbosity=1)​​​时,可以通过​​verbosity​​​参数来控制错误信息的详细程度。
​​​verbosity=0​​:

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

​verbosity=1​​:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

​verbosity=2​​:

test_assertFalse (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

由结果可以总结,verbosity有3种的错误信息状态提示报告:

  • 0,静默模式,对于测试结果给予简单提示。
  • 1,默认模式,与静默模式类似,只是在每个成功的用例前面有个​​.​​​每个失败的用例前面有个​​F​​​,跳过的用例有个​​S​​。
  • 2,详细模式,测试结果会显示每个用例的所有相关的信息。

切记,只有​​0、1、2​​​三种状态。
默认的是1。
-v
除此之外,我们在终端执行时也可以输出详细报告:

M:\tests>python36 myMain.py -v
test_assertFalse (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

如上示例,使​​verbosity​​​参数保持默认,我们通过在终端加​​-v​​来输入详细报告信息。

除了​​-v​​,还可以有:

M:\tests>python36 myMain.py -p    # 等效于verbosity=0

什么都不加,就是​​verbosity=1​​。

跳过测试用例:skip

 

从Python3.1版本开始,unittest支持跳过单个测试方法甚至整个测试类。
也就是说,某些情况下,我们需要跳过指定的用例。
我们可以使用unittest提供的相关装饰器来完成:

decorators

description

@unittest.skip(reason)

无条件地跳过装饰测试用例。 理由应该描述为什么跳过测试用例。

@unittest.skipIf(condition, reason)

如果条件为真,则跳过修饰的测试用例。

@unittest.skipUnless(condition, reason)

除非条件为真,否则跳过修饰的测试用例。

@unittest.expectedFailure

将测试标记为预期的失败。如果测试失败,将被视为成功。如果测试用例通过,则认为是失败。

expection unittest.SkipTest(reason)

引发此异常以跳过测试测试用例。

示例:

import unittest

class TestCase01(unittest.TestCase):

def test_assertTrue(self):
self.assertTrue('')

@unittest.skip('no test') # 跳过该条用例
def test_assertFalse(self):
self.assertFalse('')

@unittest.skip('no test') # 跳过这个用例类
class TestCase02(unittest.TestCase):

def test_assertTrue(self):
self.assertTrue('')

def test_assertFalse(self):
self.assertFalse('')

if __name__ == '__main__':
unittest.main()

看结果:

M:\tests>python36 myMain.py
sFss
======================================================================
FAIL: test_assertTrue (__main__.TestCase01)
----------------------------------------------------------------------
Traceback (most recent call last):
File "demo0.py", line 27, in test_assertTrue
self.assertTrue('')
AssertionError: '' is not true

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1, skipped=3)

毋庸置疑,在结果中,总共4个用例,一个用例类被跳过,另一个用例类中跳过一个方法,那么就是执行4个用例,跳过3个

再探unittest.main

 

在解释器的​​Lib\unittest​​框架内,主要目录和文件,故事将会在这里展开。

**\Lib\unittest\
├─test\ # 目录
├─case.py
├─loader.py
├─main.py
├─mock.py
├─result.py
├─runner.py
├─signals.py
├─suite.py
├─util.py
├─__init__.py
└─__main__.py

现在,我们在脚本中执行这样一段代码:

import unittest

class TestStringMethods(unittest.TestCase):

def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')

def test_isupper(self):
self.assertTrue('FOO'.isupper())

if __name__ == '__main__':
unittest.main()

当我们在终端执行:

M:\tests>python36 myMain.py -v
test_isupper (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

unittest源码是这样执行的.........

在​​main.py​​​文件中。
main = TestProgram

class TestProgram(object):
# defaults for testing
module=None
verbosity = 1
failfast = catchbreak = buffer = progName = warnings = None
_discovery_parser = None

def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None, warnings=None, *, tb_locals=False):
print(argv) # ['myMain.py', '-v']
self.parseArgs(argv) # 检查参数
self.runTests() # 执行测试用例集
main = TestProgram

首先可以看到,​​main = TestProgram​​​,所以,​​unittest.main()​​​相当于​​unittest.TestProgram()​​​。类加括号是实例化的过程,所以,我们将目光集中在​​__init__​​​方法中,为实例化对象添加属性我们先略过,主要来看在这里都是执行了哪些方法。
可以看到主要做了两件事,​​​self.parseArgs(argv)​​​检查终端是否有参数传入,是有参数​​-v​​​的。完事执行​​self.runTests()​​​。
先来研究检查参数的​​​self.parseArgs​​方法做了什么?

main.py: TestProgram.parseArgs

class TestProgram(object):
def parseArgs(self, argv):
self.createTests()

​parseArgs​​​经过一系列的操作,我们来到该方法的最后一行,​​self.createTests()​​​,见名知意,这家伙是要创建用例集啊,看看具体怎么玩的。
main.py: TestProgram.createTests

class TestProgram(object):
def createTests(self):
# self.testNames: None
# self.module: <module '__main__' from 'myMain.py'>
if self.testNames is None:
self.test = self.testLoader.loadTestsFromModule(self.module)
else:
self.test = self.testLoader.loadTestsFromNames(self.testNames, self.module)

首先判断​​self.testNames​​​是不是为None,这个参数是​​TestProgram.__init__(defaultTest=None)​​​中的​​defaultTest​​​参数,我们并没有传参,所以是None,那么就执行if条件。在if条件中执行了​​self.testLoader.loadTestsFromModule(self.module)​​​方法,并传递了​​self.module​​​参数,该参数其实就是我们运行的脚本文件名。
我们看看这个​​​self.testLoader.loadTestsFromModule​​​方法做了什么。
loader.py: TestLoader.loadTestsFromModule
​​​loadTestsFromModule​​​方法位于unittest框架下的​​loader.py​​​的​​TestLoader​​类中。

class TestLoader(object):
""" 根据各种标准生成测试用例集 """

def loadTestsFromModule(self, module, *args, pattern=None, **kws):
"""返回给定模块中用例类(可能有多个用例类)中的用例 suite """

tests = []
# dir(module)获取 myMain.py中所有顶级属性,包括类名、函数名、变量名
# 循环判断每一个属性并判断是否是case.TestCase的派生类
for name in dir(module):
obj = getattr(module, name)
# 如果是case.TestCase的派生类,就添加到tests的列表中
# 但在添加之前,要做类型检查判断
if isinstance(obj, type) and issubclass(obj, case.TestCase):
tests.append(self.loadTestsFromTestCase(obj))
# module:myMain.py
# module中没有 load_tests,所以 load_tests为None
load_tests = getattr(module, 'load_tests', None)
tests = self.suiteClass(tests)
# 因为load_tests为None,所以if语句不会执行,
if load_tests is not None:
try:
return load_tests(self, tests, pattern)
except Exception as e:
error_case, error_message = _make_failed_load_tests(
module.__name__, e, self.suiteClass)
self.errors.append(error_message)
return error_case
# tests: <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<__main__.TestStringMethods testMethod=test_isupper>, <__main__.TestStringMethods testMethod=test_upper>]>]>
return tests # 用例集 suite

def loadTestsFromTestCase(self, testCaseClass):
"""返回testCaseClass中包含的所有测试用例的 suite"""
# testCaseClass:是myMain.py中的用例类名 <class '__main__.TestStringMethods'>
if issubclass(testCaseClass, suite.TestSuite):
raise TypeError("Test cases should not be derived from "
"TestSuite. Maybe you meant to derive from "
"TestCase?")
# 获取testCaseClass中的所有以prefix指定的用例名
testCaseNames = self.getTestCaseNames(testCaseClass)
# print(testCaseClass, testCaseNames) # <class '__main__.TestStringMethods'> ['test_isupper', 'test_upper']
# 很明显,咱们的脚本中没有runTest
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
testCaseNames = ['runTest']
# 这就很明显了 self.suiteClass(map(testCaseClass, testCaseNames)) 在生成用例集的suite
loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
# loaded_suite:<unittest.suite.TestSuite tests=[<__main__.TestStringMethods testMethod=test_isupper>, <__main__.TestStringMethods testMethod=test_upper>]>
# loaded_suite.countTestCases(): 2
return loaded_suite # 返回用例集 suite

def getTestCaseNames(self, testCaseClass):
"""
返回在testCaseClass中找到的方法名的排序序列
"""

# self.testMethodPrefix:test
def isTestMethod(attrname, testCaseClass=testCaseClass,
prefix=self.testMethodPrefix):
return attrname.startswith(prefix) and \
callable(getattr(testCaseClass, attrname))

testFnNames = list(filter(isTestMethod, dir(testCaseClass)))
if self.sortTestMethodsUsing:
testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
return testFnNames # ['test_isupper', 'test_upper']

​loader.py​​​中的​​TestLoader​​中一共做了三件事:

  • 由​​main.py: TestProgram.createTests​​​方法触发了​​loader.py: TestLoader.loadTestsFromModule​​方法执行,在这个方法中,首先循环判断取出测试脚本中的所有的用例类。
  • 然后在循环判断中,如果判断测试脚本中的类是​​case.TestCase​​​的派生类,就调用​​loader.py: TestLoader.loadTestsFromTestCase​​​方法调用​​loader.py: TestLoader.getTestCaseNames​​​并将用例类传递进去,该方法获取到传过来的用例类名,然后去这个用例类中去找所有prefix开头的用例,然后以列表的形式返回给​​loader.py: TestLoader.loadTestsFromTestCase​​方法。
  • ​loader.py: TestLoader.loadTestsFromTestCase​​方法拿到用例列表后,生成用例集suite并返回调用者。

程序在​​loader.py​​​执行完毕,回到​​main.py: TestProgram.createTests​​​中。
​​​main.py: TestProgram.createTests​​​成功完成任务,生成了用例集 suite。程序再次回到了调用​​main.py: TestProgram.createTests​​​的方法中——
​​​main.py: TestProgram.parseArgs​​​,然后​​main.py: TestProgram.parseArgs​​​方法也执行完毕。程序继续回到调用处——​​main.py: TestProgram.__init__​​​方法中。
此时,创建用例集的suite完成。
程序由此继续往下执行。
main.py: TestProgram.runTests
有了用例集就要执行了,往下看。

class TestProgram(object):
def runTests(self):
# 实例化时没有传参,所以 self.catchbreak: None
if self.catchbreak:
installHandler()
# self.testRunner同样没有传参,为None
if self.testRunner is None:
# runner.TextTestRunner是runner.py中的TextTestRunner对象
self.testRunner = runner.TextTestRunner

if isinstance(self.testRunner, type):
try:
try:
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
warnings=self.warnings,
tb_locals=self.tb_locals)
except TypeError:
# didn't accept the tb_locals argument
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
warnings=self.warnings)
except TypeError:
# didn't accept the verbosity, buffer or failfast arguments
testRunner = self.testRunner()
else:
# it is assumed to be a TestRunner instance
testRunner = self.testRunner
# 实例化runner.py中的TextTestRunner类得到testRunner对象
# testRunner.run(self.test)依次执行每一个用例
# 将结果收集到self.result中
# self.test:<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<__main__.TestStringMethods testMethod=test_isupper>, <__main__.TestStringMethods testMethod=test_upper>]>]>

self.result = testRunner.run(self.test)
if self.exit:
# self.result.wasSuccessful(): <bound method TestResult.wasSuccessful of <unittest.runner.TextTestResult run=2 errors
=0 failures=0>>

sys.exit(not self.result.wasSuccessful())

执行用例没啥好说的,调用了​​runner.py: TextTestRunner.run​​方法依次执行每个用例并收集结果。

runner.py: TextTestRunner.run

class TextTestResult(result.TestResult):
""" 一个测试结果类,它可以将格式化的文本结果打印到流中 """
class TextTestRunner(object):
""" 以文本形式显示结果的测试运行器 """
def __init__(self, stream=None, descriptions=True, verbosity=1,
failfast=False, buffer=False, resultclass=None, warnings=None,
*, tb_locals=False):
""" 构造一个TextTestRunner. """
# sys.stderr: <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>
# sys.stderr将结果输出到屏幕
if stream is None:
stream = sys.stderr
self.stream = _WritelnDecorator(stream) # _WritelnDecorator:文件类的装饰对象
self.descriptions = descriptions
self.verbosity = verbosity
self.failfast = failfast
self.buffer = buffer
self.tb_locals = tb_locals
self.warnings = warnings
if resultclass is not None:
# self.resultclass: TextTestResult
self.resultclass = resultclass

def _makeResult(self):
# print(self.stream, self.descriptions, self.verbosity) # <unittest.runner._WritelnDecorator object at 0x0373D690> True 2
# 返回 TextTestResult 实例化对象
return self.resultclass(self.stream, self.descriptions, self.verbosity)

def run(self, test):
"运行给定的测试用例或测试套件"
# result: TextTestResult 实例化对象
result = self._makeResult()
registerResult(result)
'''
failfast是 TextTestRunner 的一个属性,缺省为False
作用: 如果failfast为True,一旦测试集中有测试案例failed或发生error立即终止当前整个测试执行,跳过剩下所有测试案例,也就是实现“短路测试”
'''
result.failfast = self.failfast # self.failfast: False
result.buffer = self.buffer
result.tb_locals = self.tb_locals
with warnings.catch_warnings():
if self.warnings:
# if self.warnings is set, use it to filter all the warnings
warnings.simplefilter(self.warnings)
# if the filter is 'default' or 'always', special-case the
# warnings from the deprecated unittest methods to show them
# no more than once per module, because they can be fairly
# noisy. The -Wd and -Wa flags can be used to bypass this
# only when self.warnings is None.
if self.warnings in ['default', 'always']:
warnings.filterwarnings('module',
category=DeprecationWarning,
message=r'Please use assert\w+ instead.')
startTime = time.time()
# result: TextTestResult
# TextTestResult中并没有 startTestRun,但是父类的 TestResult 中有
startTestRun = getattr(result, 'startTestRun', None)
if startTestRun is not None:
# 执行TestResult的startTestRun
startTestRun()
try:
# BaseTestSuite执行了 __call__ 方法,test加括号等于执行了 BaseTestSuite 的 run 方法
test(result)
finally:
# 用例执行完毕,触发 TestResult 的 stopTestRun 方法
stopTestRun = getattr(result, 'stopTestRun', None)
if stopTestRun is not None:
stopTestRun()
stopTime = time.time()
timeTaken = stopTime - startTime
result.printErrors()
if hasattr(result, 'separator2'):
self.stream.writeln(result.separator2)
run = result.testsRun
self.stream.writeln("Ran %d test%s in %.3fs" %
(run, run != 1 and "s" or "", timeTaken))
self.stream.writeln()

expectedFails = unexpectedSuccesses = skipped = 0
try:
results = map(len, (result.expectedFailures,
result.unexpectedSuccesses,
result.skipped))
except AttributeError:
pass
else:
expectedFails, unexpectedSuccesses, skipped = results

infos = []
if not result.wasSuccessful():
self.stream.write("FAILED")
failed, errored = len(result.failures), len(result.errors)
if failed:
infos.append("failures=%d" % failed)
if errored:
infos.append("errors=%d" % errored)
else:
self.stream.write("OK")
if skipped:
infos.append("skipped=%d" % skipped)
if expectedFails:
infos.append("expected failures=%d" % expectedFails)
if unexpectedSuccesses:
infos.append("unexpected successes=%d" % unexpectedSuccesses)
if infos:
self.stream.writeln(" (%s)" % (", ".join(infos),))
else:
self.stream.write("\n")
return result

总结:

  • 收集用例。
  • 根据用例生成测试集。
  • 运行测试集。

自定义删除用例方法

 

我们之前学习unittest.makeSuite时,学过两个添加用例的方法,但是我讲过删除用的方法了吗?并没有!现在,我们已经剖析了源码,知道了添加用例是​​addTest​​​和​​addTests​​​干的。
suite.py: BaseTestSuite:

class BaseTestSuite(object):

def addTest(self, test):
# sanity checks
if not callable(test):
raise TypeError("{} is not callable".format(repr(test)))
if isinstance(test, type) and issubclass(test,
(case.TestCase, TestSuite)):
raise TypeError("TestCases and TestSuites must be instantiated "
"before passing them to addTest()")
self._tests.append(test)

def addTests(self, tests):
if isinstance(tests, str):
raise TypeError("tests must be an iterable of tests, not a string")
for test in tests:
self.addTest(test)

可以看到,​​addTest​​​是一个一个添加,而​​addTests​​​则是for循环调用​​addTest​​​添加,本质上一样的。
让我们将目光聚集到​​​addTest​​​中,可以看到使用的是​​self._test.append(test)​​。现在,我们的删除方法也有了——把添加方法复制一份,改几个字即可:

class BaseTestSuite(object):

def addTest(self, test):
# sanity checks
if not callable(test):
raise TypeError("{} is not callable".format(repr(test)))
if isinstance(test, type) and issubclass(test,
(case.TestCase, TestSuite)):
raise TypeError("TestCases and TestSuites must be instantiated "
"before passing them to addTest()")
self._tests.append(test)

def removeTest(self, test):
# sanity checks
if not callable(test):
raise TypeError("{} is not callable".format(repr(test)))
if isinstance(test, type) and issubclass(test,
(case.TestCase, TestSuite)):
raise TypeError("TestCases and TestSuites must be instantiated "
"before passing them to addTest()")
self._tests.remove(test)

没错,你没看错,就是把​​addTest​​​复制一份,方法名改为​​removeTest​​​,完事把​​self._tests.append(test)​​​改为​​self._tests.remove(test)​​就行了。

调用也类似:

import unittest


class TestStringMethods(unittest.TestCase):

def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')

def test_isupper(self):
self.assertTrue('FOO'.isupper())

if __name__ == '__main__':
case = TestStringMethods('test_upper')
suite = unittest.TestSuite()
suite.addTest(case) # suite中有一个test_upper用例
print(suite.countTestCases()) # 1
suite.removeTest(case) # 删除掉它
print(suite.countTestCases()) # 0

将执行结果输出到文件

 

我们尝试着讲用例执行结果输出到文件中。

import unittest
class TestStringMethods(unittest.TestCase):

def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')

def test_isupper(self):
self.assertTrue('FOO'.isupper())

if __name__ == '__main__':
f = open(r'M:\tests\t1.txt', 'w', encoding='utf-8')
suite = unittest.makeSuite(TestStringMethods)
unittest.TextTestRunner(stream=f).run(suite)

生成用例报告

 

如上小节中,虽然能将结果输出到某个文件中,但更多的是根据模板生成报告,这里就来研究一下,如何生成模板报告。


发送测试报告邮件

 


unittest.mock

 

小结:在unittest中,我们需要掌握的几个类:

  • unittest.TestCase:所有测试用例的基类,给定一个测试用例方法的名字,就会返回一个测试用例实例。
  • unittest.TestSuite:组织测试用例的用例集,支持测试用例的添加和删除。
  • unittest.TextTestRunner:测试用例的执行,其中Text是以文本的形式显示测试结果,测试结果会保存到TextTestResult中。
  • unittest.TextTestResult:保存测试用例信息,包括运行了多少个测试用例,成功了多少,失败了多少等信息。
  • unittest.TestLoader:加载TestCase到TESTSuite中。
  • unittest.defaultTestLoader:等于​​unittest.TestLoader()​​。
  • unittest.TestProgram:TestProgram类名被赋值给了main变量,然后通过unittest.main()的形式调用。