unittest

unittest 是 Python 自带的类 Junit 单元测试框架。

像 Junit 之于 Java 一样,unittest 可用于单元测试,也可用于 Web 自动化测试甚至接口测试。unittest 支持测试用例/测试用例集的查找、组装,还可以在测试用例/测试用例集内共享数据,也支持根据条件筛选测试用例执行,以及自动化生成测试报告。

使用 unittest 可以快速搭建自动化测试框架进行测试。

unittest 核心组成

Test Fixture

Test Fixture 通常用来做测试用例的准备或者清理工作。比如测试开始前的数据准备或者测试结束后的数据清理等。Python 通过 setUp()、tearDown()、setUpClass()、tearDownClass() 这 4 个钩子函数(Hook)来实现测试的准备和清理工作。

Test Case

Test Case 是 unittest 的最小单元,一个 Test Case 就是一个测试用例,通常 Test Case 会继承 TestCase 这个基类。

Test Suite

Test Suite 是测试套件,就是我们常说的测试用例集,它可以包含一个或多个测试用例。

Test Loader

Test Loader 用来从提供的类(classes)和模块(modules)中生成测试用例集,默认情况下unittest 会提供一个 default test loader。

Test Runner

Test Runner 是测试执行器,用来进行测试用例的执行和测试结果的输出。

unittest 运行原理

知道了 unittest 的 5 大核心类,我们看下 unittest 的运行原理,如图所示:

image-20201209102929979

Test Cases包括一个或者多个 TestCase 类,其中保存了具体的测试过程,你可以在测试类里使用 Test Fixture,例如setUp()、tearDown() 进行测试开始前的准备和结束后的清理工作。

TestSuite包括一个或者多个 TestSuite 类,其中 TestSuite 包括了一个或多个 TestCase,也可以包括其他 TestSuite。TestSuite 通过 addTest() 或者 addTests() 方法把一个个的测试用例或者测试用例集(TestSuite)组装起来成为一个新的测试用例集。

TestLoader类加载本地或从外部文件中定义好的 TestCase 或者 TestSuites。

TestRunner包括TextTestRunner类, 它提供了运行测试的标准平台。测试运行可以通过 unittest.main() 或者 python -m unittest xxx.py 来运行。

Test Results Collector包括 TestResults 类,它为测试结果提供了一个标准容器,它存储运行的测试用例状态,例如 errors、failures、skipped,测试的结果可以直接在 Console 输出,也可以为通过其他形式输出,例如 Text、result、output。

unittest的使用

unittest简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import unittest


class TestSample(unittest.TestCase):
"""
测试类必须要继承TestCase
"""

# 测试用例默认以test开头
def test_equal(self):
self.assertEqual(1, 1)

def test_not_equal(self):
self.assertNotEqual(1, 0)


if __name__ == '__main__':
# 调用
unittest.main()

定义了一个测试类 TestSample,它继承自 unittest.TestCse 类,如果使用 unittest 框架,测试类必须要继承unittest.TestCse 类,且测试用例默认以 test 开头(实际上这个可以更改)。

测试用例有2个,分别为 test_equal 和 test_not_equal。注意测试用例在 unittest 里的表现形式是一个类方法。

执行结果:

image-20201209103457947

TestFixture 的使用

如果想在测试用例或者测试用例集开始前,执行某些操作, 在测试用例或者测试用例集结束后再执行另外一些操作,那么应该使用 Test Fixture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import unittest


class TestSample(unittest.TestCase):
"""
测试类必须要继承TestCase
"""

# 类共享的fixture,在整个测试类执行过程中仅仅执行一次,需加装饰器@classmethod
@classmethod
def setUpClass(cls):
print('整个测试类只执行一次 -- Start')

# 测试用例fixture
def setUp(self):
print('每个测试开始前执行一次')

# 测试用例默认以test开头
def test_equal(self):
self.assertEqual(1, 1)

def test_not_equal(self):
self.assertNotEqual(1, 0)

# 测试用例fixture
def tearDown(self):
print('每个测试结束后执行一次')

# 类共享的fixture,在整个测试类执行过程中仅仅执行一次,需加装饰器@classmethod
@classmethod
def tearDownClass(cls):
print('整个测试类只执行一次 -- End')


if __name__ == '__main__':
# 调用
unittest.main()

TestFixture包括如下4个方法

  • setUp()
    • setUp()方法在每一个测试用例执行测试前都会执行。
  • setUpClass()
    • setUpClass()方法仅在整个测试类开始执行前执行.setUpClass()方法必须使用 @classmethod 来装饰。
  • tearDown()
    • tearDown()方法在每一个测试用例执行后都会执行。
  • tearDownClass()
    • tearDownClass()方法仅在整个测试类结束执行后执行.tearDownClass()方法必须使用 @classmethod 来装饰。

setUp() 和 setUpClass() 通常用来进行测试前的准备工作。例如,访问数据库获得测试用例需要的数据等。

tearDown() 和 tearDownClass() 通常用来进行测试后的清理工作。例如,测试结束后删除测试产生的数据,将被测试系统恢复至之前的状态等。

执行结果如下:

image-20201209104013461

运行指定文件夹下的测试用例

在真实工作中,我们常常需要仅运行某一个测试类,或者某一个文件夹下的测试用例。此时,可以利用 unittest 的 main 函数来指定 module 运行。

unittest.main 的语法:

1
unittest.main(module='__main__', defaultTest=None, argv=None, testRunner=None, testLoader=unittest.defaultTestLoader, exit=True, verbosity=1, failfast=None, catchbreak=None, buffer=None, warnings=None)

其各个参数的含义如下:

  • module:指定待运行的 module,默认是“main”;
  • defaultTest:单个测试的名字或者多个测试名字的组合(必须要 iterable);
  • argv:传递给程序的一组变量,如果没有指定,那么系统默认使用 sys.argv;
  • testRunner:指定 unittest 的 test runner,可以是 test runner 类本身或者 test runner 类实例。默认情况下,main 函数会调用 sys.exit(),并且会在屏幕上显示测试运行错误或者成功的提示;
  • testLoader:必须是 TestLoader 类实例,默认是defaultTestLoader
  • exit:默认是 True,即测试运行完调用 sys.exit(),在交互模式下使用时可指定为 False;
  • verbosity:用于控制显示在 console 里的 log 等级,有 0、1、 2 三种,一般默认为等级 1,其中等级 2 显示的 log 最详细。

下面来看一个 discover 的例子, 假设我们的项目结构如下:

1
2
3
4
5
6
7
8
9
python-test
|--tests
|--test_one.py
|--test_two.py
|--three.py
|--__init__.py
|--main.py
|--__init__.py

test_one.py的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import unittest


class TestOne(unittest.TestCase):
def setUp(self):
print('这里写setUp的方法,测试前初始操作,通常是打开浏览器')

def testAssertNotEqual(self):
# 这里写具体的操作方法
self.assertEqual(1, 2)

def testAssertEqual(self):
# 这里写具体的操作方法
print(1)
self.assertEqual(1, 1)

def tearDown(self):
print('tearDown方法,测试后的清理工具,比如对测试产生的数据进')

test_two.py的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import unittest


class TestTwo(unittest.TestCase):
@classmethod
def setUpClass(cls):
print('整个测试类只执行一次 -- Start')

def setUp(self):
print('每个测试开始前执行一次')

# 测试用例默认以test开头
def equal_test(self):
self.assertEqual(1, 1)

def test_not_equal(self):
self.assertNotEqual(1, 0)

def tearDown(self):
print('每个测试结束后执行一次')

@classmethod
def tearDownClass(cls):
print('整个测试类只执行一次 -- End')

three.py内容如下:

1
2
3
4
5
6
7
8
9
import unittest


class Three(unittest.TestCase):

def testAssertEqual(self):
# 这里写具体的操作方法
self.assertEqual(1, 1)

main.py的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import importlib.util
import os
import unittest


# 解析tests文件夹,并且返回module的字符串列表
def get_module_name_string(file_dir):
return_list = []
for root, dirs, file in os.walk(file_dir):
for i in file:
if not (i.endswith('__init__.py') or i.endswith('.pyc')):
f = os.path.join(root, i)
mod = 'tests.' + f.split('/tests/')[1].replace('.py', '').replace('/', '.')
return_list.append(mod)
return return_list


if __name__ == "__main__":
# 定义suites
suites = unittest.TestSuite()
# 获取所有的module的string,类似package.mod的方式
mod_string_list = (get_module_name_string(os.path.join(os.path.dirname(__file__), 'tests')))
# 遍历每个mod string,import并且把它加入test case中来
for mod_string in mod_string_list:
m = importlib.import_module(mod_string)
test_case = unittest.TestLoader().loadTestsFromModule(m)
suites.addTests(test_case)
# 指定runner为TextTestRunner
runner = unittest.TextTestRunner(verbosity=2)
# 运行suites
runner.run(suites)

执行结果如下:

image-20201209115327597

可以看到,os.path.join(os.path.dirname(-file-), ‘tests’) 这个命令获取了 tests 这个文件夹的路径,然后我通过 get_module_name_string 这个方法,把 tests 文件夹下的所有 module 的string 获取出来(放到 mod_string_list 中去),接着我遍历每一个获取的 module string,把它导入并加入到 unittest 的 suites 中去,最后我指定了 runner 并且运行。

观察发现,只有test_one.py和test_two.py中的测试用例被执行了,而three.py中的测试用例没有被执行。

这是为什么呢?注意参数 TestLoader 默认情况下,Test Loader 仅仅会查找所有以“test”开头的 .py 文件,并且在运行测试用例时,仅会默认运行以“test”开头的测试方法,因为 three.py 是以“three”开头的并不是以“test”开头的,所以它没有执行。

动态查找测试用例运行

除去直接使用 unittest.main 方式加载 module 运行外,unittest 还支持通过 TestLoader 下的 discover 方法去查找测试用例。

语法如下:

1
unittest.TestLoader.discover(start_dir, pattern='test*.py', top_level_dir=None)

unittest 允许你从某个文件夹开始,递归查找所有符合筛选条件的测试用例,并且返回一个包含这些测试用例的 TestSuite 对象,unittest.TestLoader.discover 支持的参数如下:

  • start_dir:起始文件夹的路径;
  • pattern(匹配模式):默认搜索所有以“test”开头的测试文件,并把这些文件里的以“test”开头的测试用例挑选出来;
  • top_level_dir(根目录):测试模块必须从根目录导入,如果 start_dir 的位置不是根目录,那么必须显式指定 top_level_dir。

依旧是以下目录:

1
2
3
4
5
6
7
8
9
python-test
|--tests
|--test_one.py
|--test_two.py
|--three.py
|--__init__.py
|--main.py
|--__init__.py

其他文件内容不变,把 main.py 文件用 discover 的方式改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
import os
import unittest

if __name__ == "__main__":
loader = unittest.defaultTestLoader
# 生成测试用suite
suite = loader.discover(os.path.join(os.path.dirname(__file__), 'tests'), top_level_dir=os.path.dirname(__file__))
# 指定runner为TextTestRunner
runner = unittest.TextTestRunner(verbosity=2)
# 运行suite
runner.run(suite)

执行结果如下:

image-20201209123306130

运行后发现结果跟用 unittest.main 的方式一致。

按需组装测试用例

从上面的例子看到,所有以test开头的py文件且以test开头的方法都会被执行,那些又没的以test开头的我们如何执行呢?

在 unittest 中,testSuite 的组装,可以用上述的方式直接 discover,也可以用 unittest.TestSuite.addTest() 方式来添加测试用例到 TestSuite,指定测试用例执行。

还是以上例子:

1
2
3
4
5
6
7
8
python-test
|--tests
|--test_one.py
|--test_two.py
|--three.py
|--__init__.py
|--main.py
|--__init__.py

其他文件内容不变,main.py更改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import unittest

# 导入测试类
from tests.test_one import TestOne
from tests.test_two import TestTwo
from tests.three import Three

if __name__ == "__main__":
# 定义一个测试用例集
suite = unittest.TestSuite()
# 把导入进来的TestToRun这个测试类下面的测试方法加入测试用例
suite.addTest(TestOne('testAssertNotEqual'))
suite.addTest(TestTwo('equal_test'))
suite.addTest(Three('testAssertEqual'))
# 指定runner为TextTestRunner
runner = unittest.TextTestRunner(verbosity=2)
# 运行测试
runner.run(suite)

执行结果如下:

image-20201209135405825

在本次测试中,挑选了TestOne测试类中的testAssertNotEqual方法、TestTwo测试类中的equal_test方法和Three类中的testAssertEqual方法,将他们组装到一个TestSuite里运行。

通过 suit.addTest() 的方式,就可以按照需要实现把不同文件下的测试用例组装到同一个 suite 执行的操作。

破除默认 pattern,随心所欲命名测试文件

在以上的例子中,three.py文件下的测试用例都没有被执行,其原因就是 unittest 有默认的查找 pattern 如下:

  • 查找测试文件,默认查找“test*.py”;
  • 查找测试用例,默认查找“test*”。

我们可以通过更改查找 pattern 的方式来执行所有的测试用例,仍以上述项目为例:

1
2
3
4
5
6
7
8
python-test
|--tests
|--test_one.py
|--test_two.py
|--three.py
|--__init__.py
|--main.py
|--__init__.py

其他文件不变,更改 main.py 为:

1
2
3
4
5
6
7
8
9
import os
import unittest

if __name__ == "__main__":
suite = unittest.defaultTestLoader.discover(os.path.join(os.path.dirname(__file__), "tests"), pattern='*.py',
top_level_dir=os.path.dirname(__file__))
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)

执行结果如下:

image-20201209140005804

我们把默认的 pattern 更改为”*.py“,这样任何在 tests 文件夹下的 py 文件都可以被查找到。可以看到 three.py 下运行了测试用例testAssertEqual,但是test_two下的“equal_test”这个方法没有运行,那是因为方法“testMethodPrefix”在起作用。

我们来更改下测试方法的默认查找方式, 更改 main.py 为如下:

1
2
3
4
5
6
7
8
9
10
11
import os
import unittest

if __name__ == "__main__":
loader = unittest.defaultTestLoader
# 设置仅运行以equal开头的测试用例
loader.testMethodPrefix = 'equal'
suite = loader.discover(start_dir=os.path.join(os.path.dirname(__file__), "tests"), pattern='*.py',
top_level_dir=os.path.dirname(__file__))
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)

执行结果如下:

image-20201209140357878

执行的测试用例只有equal_test,可以发现testMethodPrefix改变了查找测试用例的默认方式。

忽略测试用例执行

unittest 还支持忽略执行某些测试用例,只要在要忽略的测试用例上加上如下装饰器即可:

  • @unittest.skip() 执行时直接忽略掉被装饰的测试用例;
  • @unittest.skipIf() 如果 skipIf 里的条件成立,执行时直接忽略掉被装饰的测试用例;
  • @unittest.skipUnless() 永久在执行时忽略被装饰的测试用例,除非 skipUnless 里的条件成立;
  • @unittest.expectedFailure期望被装饰的测试用例是失败的,如果是失败的,则此条测试用例将被标记为测试通过。

下面来通过一组测试来显示如何忽略测试用例执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import unittest

flag = False


# 测试类必须要继承TestCase类
class DemoTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
print('整个测试类只执行一次 -- Start')

def setUp(self):
print('每个测试开始前执行一次')

@unittest.skip('没有任何原因,忽略运行')
def equal_test(self):
self.assertEqual(1, 1)

@unittest.skipIf(flag == True, "flag为True则skip")
def test_not_equal(self):
self.assertNotEqual(1, 0)

@unittest.skipUnless(flag == True, "flag为False则skip")
def test_not_equal1(self):
self.assertNotEqual(1, 0)

@unittest.expectedFailure
def test_not_equal2(self):
self.assertNotEqual(1, 0)

def tearDown(self):
print('每个测试结束后执行一次')

@classmethod
def tearDownClass(cls):
print('整个测试类只执行一次 -- End')


if __name__ == '__main__':
flag = False
unittest.main(verbosity=2)

执行结果:

image-20201209140744888

unittest框架创建测试的步骤

  • 编写一个测试类,这个测试类必须继承 TestCase 这个基类, 测试类所对应的 .py 文件默认要以 test 开头;
  • 在这个测试类下面写你的测试方法,每个测试方法应该包括一个测试的完整步骤,测试方法要默认以 test 开头;
  • 通过 unittest.main()、runner.run() 或者 python -m 的方式来调用这些测试用例。