单元测试是针对单元内部逻辑进行的测试。单元是软件中最细小不可再分解的组成部分,单元通常是代码中方法或函数,也就是说单元测试,就是对开发人员编写的一个个方法或函数进行的测试。这样的测试必须在开发环境中进行。测试需要覆盖单元的输入输出、执行路径、边界值、数据结构、异常处理等。
单元测试的基本过程:编写一个驱动单元,该驱动单元能够将被测试的单元运行起来,给被测试单元传递所需的参数,并获取被测试单元返回的结果,最后能判断该结果是否正确。
单元测试框架不仅适用于单元测试,也适用于web自动化测试用例的开发和执行。它能更好地维护和管理测试用例,通过单元测试框架还能对测试结果统计和生成测试报告。
下面以计算器类中的加减乘除的方法为被测试对象。
# program/Calc.py
class Calculator:
def plus(self,*numbers):
sum=0
for i in numbers:
sum+=i
return sum
不用 unittest ,也可以创建驱动单元,但一般只进行最简单最基本的单元测试。
# program/TestCalc.py
from program.Calc import Calculator # 导入被测试模块的被测类对象
c = Calcultor() #创建被测类对象的实例
testSum = c.plus(5,0,2)
if testSum==7:
print('test passed')
else:
print('test failed')
开发 unittest 的测试方法时,需要用 Pycharm 新建一个模块 Testxx.py,文件类型选择 python unit test。
# program/TestCalc.py
import unittest
from program.Calc import Calculator
class MyTestCase(unittest.TestCase): # 继承基类TestCase,得到的子类就是最基本的测试类
def test_three_int_plus(self): # 一个最基本的测试类要有以test开头的测试方法
c = Calcultor()
self.assertEqual(7,c.plus(5,0,2),'Three integer addition calculation succeeded')
# 若是从当前模块执行则执行以下内容,若是被其他模块导入则不执行
if __name__ == '__main__':
unittest.main()
Unittest 是 python 内置的一个开源的单元测试的自动化测试框架。利用该开发包可以开发测试类、类中的测试方法、setUp方法、tearDown方法,可以自动执行测试类中的测试方法,并能检查实际结果是否符合预期结果。还能和其他第三方的扩展开发包相结合,使用最多的就是ddt数据驱动测试的开发包、xlrd读xls的开发包,配合起来能够实现自动化测试的参数化。我的文章部分内容依据了 Unittest 官方文档编写,感兴趣也可以查阅更多内容。
Unittest 通过面向对象支持了围绕四个核心概念的内容:
Test case(测试用例):单元测试中最小的不可再分的个体
Test suite(测试集):是 Test case 的集合,用来聚合 Test case 放在一起执行
Test Fixture(测试固件):测试运行前的准备(setUp)工作和测试结束后的清理(tearDown)工作
Test Runner(测试运行器):可以执行 Test case 并提供测试结果
Unittest 提供一个 unittest.TestCase 基类来新建测试用例;
即测试用例表示为 unittest.TestCase
的实例(子类);
class MyTestCase(unittest.TestCase):
TestCase 最简单的子类需要实现一个测试方法(一个命名以 test 开头的方法)以执行特定的测试代码。
import unittest
class DefaultWidgetSizeTestCase(unittest.TestCase):
def test_default_widget_size(self):
widget = Widget('The widget')
self.assertEqual(widget.size(), (50, 50))
以下是测试 string 字符串的upper(),isupper(),split()三个方法的基本实例。
调用 assertEqual() 来检查预期的输出; 调用 assertTrue() 或 assertFalse() 来验证一个条件;调用 assertRaises() 来验证抛出了一个特定的异常。
import unittest
class TestStringMethods(unittest.TestCase): #继承 unittest.TestCase,它的实例就继承到了断言方法
def test_upper(self): #该测试类的三个测试方法必须都是 test 开头
self.assertEqual('hi'.upper(), 'HI')
def test_isupper(self):
self.assertTrue('HI'.isupper())
self.assertFalse('HI'.isupper())
def test_split(self):
str = 'hello world'
self.assertEqual(str.split(), ['hello', 'world'])
# 若 str.split() 分割结果不是该字符串,测试失败
with self.assertRaises(TypeError):
str.split(2)
if __name__ == '__main__':
unittest.main() #提供了一个测试脚本的命令行接口,生成一些输出结果
当然,继承于 unittest.TestCase 的子类或实例的测试方法,不止assertEqual(),assertTrue(),assertRaises()这些,大致有三组:
一组用于运行测试的方法,另一组用于检查条件和报告故障的断言方法,还有一些收集测试本身信息的查询方法。
setUp
()和tearDown
()
在每个测试方法调用之前(或之后)就要调用的方法。
setUp
() 通常用于 test fixture (测试固件)的准备,泛指每个测试方法执行前做的准备工作,比如测试数据的分割分派。
tearDown
() 通常用于写测试报告,在每个测试方法运行后进行清理(tear down)工作。
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self): #表示每个测试方法执行之前先执行的方法
self.widget = Widget('The widget')
def tearDown(self): #表示每个测试方法执行之后再执行的方法
self.widget.dispose()
注:对多个前置/后置操作相同的测试,运行测试时,测试框架会自动为每个单独测试调用对应的前置/后置方法。
setUpClass
()和tearDownClass
()
在每个测试类运行之前(或之后)就要调用的方法。
都使用cls
作为唯一参数调用,而且都只能用@classmethod
装饰器。
@classmethod
def setUpClass(cls): #表示每个测试类执行之前先执行的方法
...
@classmethod
def tearDownClass(cls): #表示每个测试类执行之后再执行的方法
...
都必须作为类方法实现:
import unittest
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._connection = createExpensiveConnectionObject()
@classmethod
def tearDownClass(cls):
cls._connection.destroy()
setUpClass(cls)
主要是针对该测试类执行前做的准备工作,比如准备测试数据,执行sql语句,读取xls文件等;
tearDownClass(cls)
主要是针对该测试类完成后再做的收尾工作,比如保存文件、关闭文件,断开数据库连接、保存生成测试报告、发送测试完成的通知邮件等。
TestCase 类提供了不同断言方法assertXX()
来检查条件和报告故障。如果断言方法的结果,满足断言条件则测试通过,不满足条件则断言失败,测试不通过。这些断言方法都提供了一个消息可选参数 msg=none,默认空。
方法 | 测试通过的条件 | 备注 |
---|---|---|
assertEqual(a, b) | a == b |
断言两值相等 |
assertNotEqual(a, b) | a != b |
断言两值不等 |
assertTrue(x) | bool(x) is True |
断言值为真 |
assertFalse(x) | bool(x) is False |
断言值为假 |
assertAlmostEqual(a, b) | round(a-b, 7) == 0 |
断言两浮点型数字近似相等 |
assertNotAlmostEqual(a, b) | round(a-b, 7) != 0 |
断言两浮点型数字不近似相等 |
assertIs(a, b) | a is b |
断言两对象是同一对象 |
assertIsNot(a, b) | a is not b |
断言两对象是不同对象 |
assertIsNone(x) | x is None |
断言x为none |
assertIsNotNone(x) | x is not None |
断言x非none |
assertIn(a, b) | a in b |
断言对象是另个对象的部分或成员 |
assertNotIn(a, b) | a not in b |
断言对象不是另个对象的部分或成员 |
assertIsInstance(obj, cls) | isinstance(a, b) |
断言对象是另一个类的实例 |
assertNotIsInstance(obj, cls) | not isinstance(a, b) |
断言对象不是另一个类的实例 |
实际环境示例
其中 assertEqual(a,b)
会自动使用以下类型的特定方法。
方法 | 比较类型 | 备注 |
---|---|---|
assertMultiLineEqual(a, b) | strings | 断言字符串是否相等 |
assertSequenceEqual(a, b) | sequences | 断言序列是否相等 |
assertListEqual(a, b) | lists | 断言列表是否相等 |
assertTupleEqual(a, b) | tuples | 断言元组是否相等 |
assertSetEqual(a, b) | sets or frozensets | 断言集合是否相等 |
assertDictEqual(a, b) | dicts | 断言字典是否相等 |
还有其他用于执行更具体检查的方法,分别测试 a >,>=,<,<= b,如果不是,则测试将失败。
方法 | 测试通过的条件 | 备注 |
---|---|---|
assertGreater(a, b) | a > b |
|
assertGreaterEqual(a, b) | a >= b |
|
assertLess(a, b) | a < b |
|
assertLessEqual(a, b) | a <= b |
可用以下方法检查异常(exception),警告(warn),日志(log)消息的产生(raise)。
方法 | 产生了指定异常/警告/日志则测试通过 | 备注 |
---|---|---|
assertRaises(exc, callable, *args, **kwds) | fun(*args, **kwds) raises exc |
断言产生了指定异常 |
assertRaisesRegex(exc, r, fun, *args, **kwds) | fun(*args, **kwds) raises exc and the message matches regex r |
|
assertWarns(warn, fun, *args, **kwds) | fun(*args, **kwds) raises warn |
|
assertWarnsRegex(warn, r, fun, *args, **kwds) | fun(*args, **kwds) raises warn and the message matches regex r |
|
assertLogs(logger, level) | The with block logs on logger with minimum level |
Unittest 中使用self.assertRaises()
断言方法来判断是否产生了指定异常:
with self.assertRaises(指定异常类) as cm:
do_something() #执行的测试
the_exception = cm.exception
self.assertEqual(the_exception.error_code, 3)
一个测试代码的运行环境叫做 Testfixture 测试固件,代表执行多个测试所需的环境准备和清理动作。如创建临时或代理数据库、目录或启动服务器进程。一个新的 TestCase 的子类实例作为一个测试固件,用来运行每个独立的测试方法。在运行每个测试时,setUp() 、tearDown()
和 __init__()
会被调用一次。以下是我写的一个关于测试固件结构的简单示例:
以 unittest.TestSuite
类为代表,根据所测试功能,把 TestCase对象中多个测试类的测试方法添加到 TestSuite 测试集;用于将多个测试类批量执行和归档需要一起执行的测试。
虽然,大部分情况下调用 unittest.main()
也会集合所有模块的测试用例并执行;
但是,如果需要自定义自己的测试套件的话,可以用以下内容组织自己的测试:
TestSuite对象
TestSuite对象.addTest(测试类(“测试方法名”))
方法,目的是把所需执行测试的测试类中的测试方法添加到测试集TextTestRunner对象.run(一个测试集对象)
方法,目的是把测试集里的所有测试方法按顺序全部执行def mySuite():
# mySuite 作为 unittest.TestSuite 的一个对象
mySuite = unittest.TestSuite()
# mySuite 的 addTest方法,向 mySuite 测试集中添加:测试类中的测试方法
mySuite.addTest(WidgetTestCase('test_default_widget_size'))
mySuite.addTest(WidgetTestCase('test_widget_resize'))
# mySuite.addTest(测试类('test_测试方法名'))...
return mySuite
if __name__ == '__main__':
# myRunner 作为 unittest.TextTestRunner 的一个对象
runner = unittest.TextTestRunner()
# myRunner 的 run方法,把 mySuite 测试集里已添加的所有测试方法执行掉
myRunner.run(mySuite())
实际环境示例
探索性测试在 unittest.defaultTestLoader.discover
中实现
def discover(self, start_dir, pattern='test*.py', top_level_dir=None): # unittest code
作用:将指定目录下的特定文件名的测试模块当中,所有测试类的所有测试方法全部执行。
首先创建一个defaultTestLoader.discover()对象
,一般只需要传start_dir
参数:
start_dir
是指定的目录,要运行当前目录,则目录参数为”./”
pattern='test*.py'
表示执行模块的文件名,默认值”test*.py”
top_level_dir=None
表示顶层目录,一般不设置。
然后执行TextTestRunner对象.run(一个discover对象)
import unittest
import testFolders.testModules # 导入指定测试目录下的测试模块.py
if __name__=="__main__":
# myDiscover 作为 unittest.defaultTestLoader.discover 一个对象
myDiscover = unittest.defaultTestLoader.discover('./')
# myRunner 作为 unittest.TextTestRunner 的一个对象
myRunner = unittest.TextTestRunner()
# myRunner 的 run方法,把 myDiscover 里已添加的所有测试方法执行掉
myRunner.run(myDiscover)
实际环境示例
以下的装饰器可以用来装饰想跳过不执行的测试方法
@unittest.skip() #装饰一个测试方法,表示直接跳过该方法,不执行该方法。
@unittest.skipIf(条件) #满足条件才跳过该测试方法
@unittest.skipUnless(条件) #不满足条件才跳过该测试方法
@unittest.expectedFailure #装饰一个测试方法,表示把测试标记为预计失败。
实际环境示例,测试结果显示 skip 了指定 test case
实例化 unittest.TextTestRunner
对象,得到的这个运行器用于执行和输出测试结果,可能使用图形、文本接口,或返回一个特定值表示运行测试的结果。
if __name__ == '__main__':
myRunner = unittest.TextTestRunner() # 创建一个 unittest.TextTestRunner 测试运行器对象
myRunner.run(mySuite()) # 调用 myRunner 的 run 方法
博主CSDN唯一首发,原创手动码字,喜欢就点赞收藏