随着自动化脚本数量的增加,用例及测试数据的组织和维护,公共模块的复用,用例挑选及执行控制,或者团队协作及用例编写规范化,我们便需要引入自动化测试框架。
框架是应用的组织架构,一般包含代码、配置、数据、日志、依赖的组织,可复用模块的抽取以及运行控制等。就像从一盘散沙的武装人员,到一个军队。框架是由脚本集合发展到应用(包含测试项目)的一种必然选择。
框架的基本功能一般包括:
- 代码、配置文件、数据文件等的分类组织。
- 依赖管理。
- 公共模块的复用。
- 运行流程及控制。
另外,从设计目标来看,框架应该具有易用、健壮稳定、良好的性能,通过配置或参数提供灵活的使用方式,以及易调试和维护(如提供运行日志)等特性。
测试框架是为测试而设计的框架。测试框架一般还要包含用例编写、丰富的断言方法、用例组织、测试准备和清理方法、执行测试、生成测试报告等功能。
相比于一堆脚本集合,测试框架一般具有以下优点。
- 用例执行互相独立,一个用例失败不影响其他用例执行和验证。
- 清晰的用例执行状态,如成功、失败、出错、跳过等以及系统的测试报告。
- 灵活的用例挑选及批量运行。
- 提供丰富的断言(期望结果与实际结果的对比)方法。
- 提供不同范围的测试准备和清理方法。
- 在特定环境不满足条件时,跳过用例。
Python中常用的测试框架有Unittest、Nose、Pytest以及Robot Framework等。其中Unittest是Python自带的测试框架,提供基础的用例管理和测试控制功能,使用灵活易于定制。本章以unittest及Selenium为基础来讲解Web自动化测试框架的搭建流程。Pytest和Robot Framework将在其他章进行讲解。
9.1 Unittest基本使用
Unitest是Python自带的单元测试框架,依照JUnit编写,和其他语言的的主流单元测试框有着相似的风格。Unittest中主要包含TestCase测试用例、TestSuite测试套件、TestFixture测试准备及清理方法和TestRunner测试运行器等主要概念,另外还包含TestLoader用于批量加载用例生成测试套件,TestResult用于在TestRunner中记录测试结果。Unittest的模型关系及运作流程如图9.1所示。
图9.1 Unittest模型关系及运作流程
编写测试用例时,需要编写一个类(一般约定以Test开头),并继承unittest.TestCase。每个测试类包含测试方法(测试用例)及TestFixture测试准备和清理方法两部分。测试方法一般约定为名称已test开头(测试类中非已test开头的方法不作为用例执行,一般可以用作辅助步骤等)。每个测试方法在执行时会实例化为一个TestCase对象来运行。同时测试用例父类unittest.TestCase中提供了丰富的断言(测试对比)方法来验证结果。
TestFixture也译做测试脚手架或者测试夹具。包含setup测试准备和teardown测试清理两类方法,分别在用例执行前执行和用例执行后执行,像三明治的两片面包一样将我们的正菜测试用例夹在其中。TestFixture提供不同范围的测试准备和清理,包含测试用例级、测试类级和测模块级。
TestSuite测试套件用来挑选和组织用例。除了使用测试套件自带的addTest添加用例方法添加用例外,还可以使用TestLoader用例加载器对象来批量添加一个测试模块、一个测试类等快速生成一个测试套件对象。同时测试套件支持嵌套,执行时按用例及子套件添加顺序进行深度遍历执行。
TestRunner用于测试套件、其中的用例各个级别的TestFixture的运行控制及异常处理。TestRunner中包含一个TestResult对象,穿梭于各个用例及套件中记录并实时输出结果。
用例编写
Unittest测试用例编写一般应遵循以下步骤。
(1)测试脚本(测试模块)约定以test开头。
(2)测试类约定以Test开头,并继承unittest.TestCase。
(3)测试方法(测试用例)约定以test开头,可以包含多个步骤,及多个断言。
注意:测试类中不应覆盖父类的__init__方法,否则会破坏Unittest将每个测试方法生成测试用例对象的过程。
测试类TestCase
测试类编写示例如下。
import unittest
class TestDemo(unittest.TestCase):
"""测试类示例"""
def add(self, a, b): # 非test开头不作为用例
return a + b
def test_add_int(self):
"""测试整数相加""" # 使用docstring进行用例描述
s = self.add(1, 2)
self.assertEqual(s, 3) # 实际结果, 期望结果
def test_add_float(self):
"""测试浮点数相加"""
s = self.add(10000.1, 0.1)
self.assertEqual(s, 10000.2)
def test_add_str(self):
"""测试字符串相加"""
s = self.add('10', '1')
self.assertEqual(s, '11')
if __name__ == '__main__':
unittest.main()
上例测试类中的add方法非test开头,不作为用例运行,一般测试类中的非测试方法可以作为辅助步骤供测试方法调用。
测试方法可以使用docstring(一对三个双引号中间的字符串),来提供用例描述。用例描述会详细模式下显示。self.assertEqual(s,3)
是父类unittest.TestCase
提供的一种断言方法,用于断言相等。在用例中也可以直接使用Python自带的assert语句,如assert s==3
。但是相比来说使用TestCase类提供的断言方法,在失败信息中可以查看到更清晰的结果对比。if __name__ == '__main__'
表示如果本模块独立执行(非其他模块调用),unittest.main()
为当前脚本提供一个命令行接口(支持多种运行参数),在运行脚本时默认运行当前测试模块按约定规则(测试类以Test开头,测试方法以test开头)所能发现的所有用例。
执行脚本,运行结果如下。
..F
======================================================================
FAIL: test_add_str (__main__.TestDemo)
测试字符串相加
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/superhin/Desktop/test_demo.py", line 22, in test_add_str
self.assertEqual(s, '11')
AssertionError: '101' != '11'
- 101
? -
+ 11
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
最上面的“..F”表示,前两条用例通过,第三条用例失败。unittest.main()中可以通过参数verbosity来控制显示结果的详细级别,支持0,1,2三级,默认为1。此时通过的用例显示为“.”,失败的用例显示为“F”,出错的用例显示为“E”。失败和出错的区别为失败始终实际结果和期望结果不一致导致的AssertionError断言异常。出错指用例运行中遇到了其他未捕获的异常而时用例执行*中断。
当用例失败或出错时会显示详细的追溯信息及运行中的实际结果与期望结果。如上例中在断言下展示了实际结果是“101”而期望结果是“11”,因此导致了断言失败。
运行最后是运行概要信息。如上例共运行3条用例,用时0.001秒,总体状态为失败,失败用例1条。
unittest.main()参数verbosity为0时,不显示每条用例的执行情况,只显示报错信息。verbosity=2
时则显示每条用例的名称或描述及执行情况。
注意:用例执行顺序并非按定义编写顺序执行,而是按用例方法名的ASCII码排序后执行。
修改unittest.main()为unittest.main(verbosity=2)
,重新运行结果如下。
test_add_float (__main__.TestDemo)
测试浮点数相加 ... ok
test_add_int (__main__.TestDemo)
测试整数相加 ... ok
test_add_str (__main__.TestDemo)
测试字符串相加 ... FAIL
======================================================================
FAIL: test_add_str (__main__.TestDemo)
测试字符串相加
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/superhin/Desktop/test_demo.py", line 22, in test_add_str
self.assertEqual(s, '11')
AssertionError: '101' != '11'
- 101
? -
+ 11
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
在verbosity=2详尽结果模式下,每条用会显示其测试方法名,所属模块及测试类、docstring测试描述以及执行状态。“ok”表示通过,“FAIL”表示失败。
注:除了使用类来编写测用例外,unittest还支持将函数形式的测试代码转换为测试用例对象,示例如下。
import unittest
def test_add_zero():
s = 0 + 0
assert s == 0
testcase = unittest.FunctionTestCase(test_add_zero, setUp=..., tearDown=...)
一般来说,推荐使用测试类的写法。
断言方法
表9.1 TestCase常用断言方法
断言方法 | 解释 | 示例 |
---|---|---|
assertEqual(a, b) | 断言a和b值相等 | self.assertEqual(1+1, 2) |
assertNotEqual(a, b) | 断言a和b值不相等 | self.assertEqual(1+2, 2) |
assertTrue(x) | 断言x是True | self.assertTrue(2>1) |
assertFalse(x) | 断言x的False | self.assertTrue(2<1) |
assertIs(a, b) | 断言a和b是同一个对象 | self.assertIs(1, 1) |
assertIsNot(a, b) | 断言a和b不是同一个对象 | self.assertIsNot(1, True) |
assertIsNone(x) | 断言x是None | self.assertIsNone({}.get('a'), None) |
assertIsNotNone(x) | 断言x不是None | self.assertIsNotNone({'a':1}.get('a'), None) |
assertIn(a, b) | 断言a在b中 | self.assertIn('a', 'abc') |
assertNotIn(a, b) | 断言a不在b中 | self.assertNotIn(1, [2,3,4]) |
assertIsInstance(a, b) | 断言a是b类型 | self.assertIsInstance(1, int) |
assertNotIsInstance(a, b) | 断言a不是b类型 | self.assertNotIsInstance([], dict) |
子用例subTest和ddt数据驱动
在测试中,对同一测试流程使用不同的测试数据是否非常必要的。
当同一个用例需要测试多组数据时,在循环时为防止某组数据测试失败导致执行中断而后面的用例未进行测试可以在循环中使用subTest来确保所有数据都被执行。示例如下。
import unittest
data = [1,2,3,4,5]
class TestsDemo2(unittest.TestCase):
def test_greater(self):
"""测试数字大于2"""
for item in data:
with self.subTest(item=item):
self.assertGreater(item, 2)
if __name__ == '__main__':
unittest.main(verbosity=2)
执行时会显示所有失败的数据。
test_a (__main__.TestsDemo2)
测试数字大于2 ...
======================================================================
FAIL: test_a (__main__.TestsDemo2) (item=1)
测试数字大于2
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/superhin/Desktop/test_demo2.py", line 10, in test_a
self.assertGreater(item, 2)
AssertionError: 1 not greater than 2
======================================================================
FAIL: test_a (__main__.TestsDemo2) (item=2)
测试数字大于2
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/superhin/Desktop/test_demo2.py", line 10, in test_a
self.assertGreater(item, 2)
AssertionError: 2 not greater than 2
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=2)
使用subTest后多条数据仍表现为一条用例,如果想一条用例根据不同的数据动态生成多条用例,则可以使用三方库ddt实现。安装方法如下。
pip install ddt
使用示例如下。
import unittest
import ddt
data = [1,2,3,4,5]
@ddt.ddt
class TestsDemo3(unittest.TestCase):
@ddt.data(*data) # *data将列表解包成多个数据
def test_greater(self, item): # item用于接收@ddt.data中的每一个数据
"""测试数字大于2"""
self.assertGreater(item, 2)
if __name__ == '__main__':
unittest.main(verbosity=2)
使用ddt需要在测试类上添加装饰器@ddt.ddt,需要数据驱动的测试方法上添加装饰器@ddt.data(数据1, 数据2, 数据3, ...)
,用例中添加参数来接收每一个数据。
运行时将动态生成多条用例,实际运行结果如下。
test_greater_1_1 (__main__.TestsDemo3)
测试数字大于2 ... FAIL
test_greater_2_2 (__main__.TestsDemo3)
测试数字大于2 ... FAIL
test_greater_3_3 (__main__.TestsDemo3)
测试数字大于2 ... ok
test_greater_4_4 (__main__.TestsDemo3)
测试数字大于2 ... ok
test_greater_5_5 (__main__.TestsDemo3)
测试数字大于2 ... ok
======================================================================
FAIL: test_greater_1_1 (__main__.TestsDemo3)
测试数字大于2
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/lib/python3.7/site-packages/ddt.py", line 182, in wrapper
return func(self, *args, **kwargs)
File "/Users/superhin/Desktop/test_demo3.py", line 11, in test_greater
self.assertGreater(item, 2)
AssertionError: 1 not greater than 2
======================================================================
FAIL: test_greater_2_2 (__main__.TestsDemo3)
测试数字大于2
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/lib/python3.7/site-packages/ddt.py", line 182, in wrapper
return func(self, *args, **kwargs)
File "/Users/superhin/Desktop/test_demo3.py", line 11, in test_greater
self.assertGreater(item, 2)
AssertionError: 2 not greater than 2
----------------------------------------------------------------------
Ran 5 tests in 0.001s
FAILED (failures=2)
对于嵌套的数据,如。
import unittest
import ddt
data = [(1,2,3), (0, 0, 1), (1.1, 2.0, 3), (1, -1, 0)]
@ddt.ddt
class TestsDemo4(unittest.TestCase):
@ddt.data(*data)
def test_add(self, item):
"""测试加法"""
a, b, s = item
self.assertEqual(a+b, s)
if __name__ == '__main__':
unittest.main(verbosity=2)
也可以使用@ddt.unpack解包,自动将每一组数据拆分成多个变量,示例如下。
import unittest
import ddt
data = [(1,2,3), (0, 0, 1), (1.1, 2.0, 3), (1, -1, 0)]
@ddt.ddt
class TestsDemo4(unittest.TestCase):
@ddt.data(*data)
@ddt.unpack
def test_add(self, a, b, s):
"""测试加法"""
self.assertEqual(a+b, s)
if __name__ == '__main__':
unittest.main(verbosity=2)
两个示例执行效果相同。
用例跳过及期望失败
Unittest测试用例支持在某些条件不满足时跳过测试用例或者整个测试类。可以设置无条件跳过(比如功能尚未实现、废弃或者Bug未修复)或者根据条件跳过(比如环境条件不具备、依赖步骤失败等),官网示例如下。
class MyTestCase(unittest.TestCase):
@unittest.skip("demonstrating skipping")
def test_nothing(self):
self.fail("shouldn't happen")
@unittest.skipIf(mylib.__version__ < (1, 3),
"not supported in this library version")
def test_format(self):
# Tests that work for only a certain version of the library.
pass
@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
def test_windows_support(self):
# windows specific testing code
pass
def test_maybe_skipped(self):
if not external_resource_available():
self.skipTest("external resource not available")
# test code that depends on the external resource
pass
执行结果如下。
test_format (__main__.MyTestCase) ... skipped 'not supported in this library version'
test_nothing (__main__.MyTestCase) ... skipped 'demonstrating skipping'
test_maybe_skipped (__main__.MyTestCase) ... skipped 'external resource not available'
test_windows_support (__main__.MyTestCase) ... skipped 'requires Windows'
----------------------------------------------------------------------
Ran 4 tests in 0.005s
OK (skipped=4)
有些用例是反向用例,即正常情况下用例就应该执行失败,此时实际是符合我们的预期的(测试通过),此时为了结果展现的正确性,我们需要标记用例为期望失败,示例如下。
class ExpectedFailureTestCase(unittest.TestCase):
@unittest.expectedFailure
def test_fail(self):
self.assertEqual(1, 0, "broken")
测试准备及清理
在测试中测试准备和清理是执行测试必不可少的一部分。测试准备一般称为setUp,测试清理一般称为tearDown(一般也译作拆卸)。Unittest提供三种范围的测试准备和清理方法,分别为。
- setUp()、tearDown():测试方法(用例)级,类中的每个用例执行前和执行后执行。
- setUpClass()、tearDownClass():测试类级,本类全部用例执行前,和全部执行后执行一次。
- setUpModule()、tearDownModule():测试模块级,本测试模块所有用例执行前、所有用例执行后执行一次。
示例如下。
import unittest
def setUpModule(): # 当前测试模块所有用例前执行一次
print('=== 模块级准备 ===')
def tearDownModule(): # 当前测试模块所有用例后执行一次
print('=== 模块级清理 ===')
class TestDemo5(unittest.TestCase):
@classmethod
def setUpClass(cls): # 当前类, 所有用例执行前执行, 仅执行一次
print('--- 测试类级准备 ---')
@classmethod
def tearDownClass(cls): # 当前类, 所有用例执行后执行, 仅执行一次
print('--- 测试类级清理 ---')
def setUp(self): # 当前类,每个用例执行前执行一次
print('... 测试方法级准备 ...')
def tearDown(self): # 当前类,每个用例执行后执行一次
print('... 测试方法级请清理 ...')
def _teardown_test_a(self):
print('执行test_a清理')
def test_a(self):
print('执行test_a准备') # 用例单独的测试准备方法, 可以放在测试用例中,
print('执行test_a')
self.addCleanup(self._teardown_test_a)
def test_a_02(self):
print('执行test_b')
if __name__ == "__main__":
unittest.main(verbosity=0)
其中setUpModule和tearDownModule
是模块中的函数,无参无返。setUpClass和tearDownClass
必须是类方法,使用@classmethod
装饰。setUp
和tearDown
是正常的实例(对象)方法。
用例单独的测试准备可以写在用例方法中,正式步骤之前。用例单独的测试清理方法则建议使用self.addCleanup(function, *args, **kwargs)
来添加,以防止断言失败或测试步骤异常导致清理方法不被执行。
运行结果如下。
=== 模块级准备 ===
--- 测试类级准备 ---
... 测试方法级准备 ...
执行test_a准备
执行test_a
... 测试方法级请清理 ...
执行test_a清理
... 测试方法级准备 ...
执行test_b
... 测试方法级请清理 ...
--- 测试类级清理 ---
=== 模块级清理 ===
----------------------------------------------------------------------
Ran 2 tests in 0.001s
注意:setUpModule、tearDownModule、setUpClass、tearDownClass、setUp、tearDown这些是固定的关键字,大小写敏感,名称不能写错。
setUp、setUpClass、setUpModule方法异常时,其范围内的测试用例将不会执行。而无论用例是否异常,tearDown、tearDownClass、tearDownModule都会执行。
用例组织及加载
实际上用例很少单独执行。有时我们需要运行所有用例,或者通常我们需要挑选一批用例进行执行。为了实现跨测试类、跨测试模块来挑选用例,便引入了TestSuite测试套件的概念。TestLoader则可以快速将测试模块、测试类及一批用例名称加载生成测试套件对象。
用例组织TestSuite
TestSuite是有序的用例合集,同时嵌套子的套件,即可以快速将两个不同的测试套件组合成一个新的测试套件。
TestSuite可以通过addTest添加用例或者子套件,或者通过addTests一次添加多个用例或子套件。另外也可以在实例化时通过列表传入要添加的用例及子套件,完成添加。
示例如下。
import unittest
class TestDemo6(unittest.TestCase):
def test_01(self):
pass
def test_02(self):
pass
def test_03(self):
pass
suite1 = unittest.TestSuite()
suite1.addTest(TestDemo6('test_02')) # addTest添加用例
suite2 = unittest.TestSuite()
suite2.addTest(suite1) # addTest也可添加子套件
suite2.addTests([TestDemo6('test_03'), TestDemo6('test_01')]) # addTest批量添加用例或子套件
suite3 = unittest.TestSuite([suite1, suite2, TestDemo6('test_03')]) #实例化suite时列表传入用例及子套件
注意:不同于unittest.main()将用例按用例方法名ASCII顺序执行,测试套件是按添加顺序迭代执行的。
用例加载TestLoader
TestLoader即用例加载器,用于收集用例并生成测试套件对象。常用的方法如下。
- discover(start_dir, pattern='test*.py', top_level_dir=None):按目录递归搜集用例,从start_dir目录及所有子包中,按指定pattern匹配的脚本文件中收集测试用例,并生成具有嵌套结构的测试套件对象。
- loadTestsFromModule(module, pattern=None):加载一个测试模块中的所有测试用例,生成测试套件对象。
- loadTestsFromTestCase(testCaseClass):加载一个测试类中的所有测试用例,生成测试套件对象
- loadTestsFromName(name, module=None):按描述名称搜集测试用例,生成测试套件对象。
- loadTestsFromNames(names, module=None) 按多个描述名称搜集测试用例,生成的测试套件对象。
使用示例如下。
# 文件名: test_demo7.py
import unittest
class TestDemo7(unittest.TestCase):
def test_01(self):
pass
def test_02(self):
pass
def test_03(self):
pass
if __name__ == "__main__":
loader = unittest.TestLoader()
suite1 = loader.discover('.') # 搜集当前目录及子包下的默认以test开头的py文件中的所有用例
suite2 = loader.loadTestsFromModule('test_demo7') # 搜集本模块也支持使用__name__
suite3 = loader.loadTestsFromTestCase(TestDemo7)
suite4 = loader.loadTestsFromName('test_demo7.TestDemo7.test_01')
suite4 = loader.loadTestsFromNames(
['test_demo7.TestDemo7.test_02', 'test_demo7.TestDemo7.test_03'])
以上示例中演示了用力加载器的几种使用方法。除了loadTestsFromTestCase是使用类对象外,其他都使用字符串格式的描述来描述要导入的模块或用例发现路径。
discover一般用的比较多,可以快速遍历中一个目录中的所有用例,以及子包的用例,注意子包中要包含__init__.py文件才会被遍历。
用例运行
除了调试时使用unittest.main()运行测试模块所有的用例外,还可以通过unittest的命令行接口发现并运行用例。或者自己编写运行脚本,使用TestRunner对象运行组装好的测试套件。
命令行运行
由于unittest没有额外生成可执行文件,因此调用unittest可以使用python -m unittest或者python3 -m unittest命令。
常用的命令如下。
- python -m unittest tests:遍历运行整个tests目录及子包用例
- python -m unittest discover tests:同上,遍历运行整个tests目录及子包用例,支持discover参数。
- python3 -m unittest tests/test_demo5.py:执行tests目录下的test_demo5.py中的所有用例。
- python3 -m unittest tests.test_demo5 tests.test_demo6:执行多个测试模块
- python3 -m unittest tests.test_demo5.TestDemo5:执行指定测试模块的指定测试类。
- python3 -m unittest tests.test_demo5.TestDemo5.test_a:执行指定测试模块,指定测试类中的指定用例。
unittest命令行还支持以下运行参数。
- -v:详细结果模式。
- -b,--buffer:捕获用例中的print信息并独立输出。
- -f,--failfast:遇到出错或失败立即停止(而不运行完所有用例)。
- -k:运行用例路径(包.模块.类.测试方法)中包含指定字符串的用例,大小写敏感,支持*通配符,如-k foo可以匹配到 foo_tests.SomeTest.test_something 和 bar_tests.SomeTest.test_foo ,但是不能匹配到 bar_tests.FooTest.test_something 。
- --local:在报错回溯信息中显示测试方法的局部变量。
代码运行
代码运行即使用自定义的运行脚本,自定义组装测试套件,并使用TestRunner运行。
基本示例如下。
import unittest
def run():
suite = unittest.defaultTestLoader.discover('.', pattern='test*.py')
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
if __name__ == "__main__":
run()
生成HTML报告
由于默认的TextTestRunner只能输出文本形式的测试报告,要生成HTML形式的报告我们需要三方的插件,相关的插件比较多,如单文件的HTMLTestRunner.py、可以直接安装的三方库htmlrunner、htmlreport等,这里以htmlrunner为例讲解。
安装方法为。
pip install htmlrunner
简单使用方法如下。
import unittest
import htmlrunner
def run():
suite = unittest.defaultTestLoader.discover('.', pattern='test*.py')
runner = htmlrunner.HTMLRunner(report_file='%Y%M%d.html', title='测试报告', description='测试报告描述')
runner.run(suite)
if __name__ == "__main__":
run()
默认报告样式如图9.2所示。
图9.2 htmlrunner测试报告默认样式