Python Unittest 单元自动化测试框架

一、单元测试

1.单元测试概念

单元测试是针对单元内部逻辑进行的测试。单元是软件中最细小不可再分解的组成部分,单元通常是代码中方法或函数,也就是说单元测试,就是对开发人员编写的一个个方法或函数进行的测试。这样的测试必须在开发环境中进行。测试需要覆盖单元的输入输出、执行路径、边界值、数据结构、异常处理等。

2.单元测试过程

单元测试的基本过程:编写一个驱动单元,该驱动单元能够将被测试的单元运行起来,给被测试单元传递所需的参数,并获取被测试单元返回的结果,最后能判断该结果是否正确。

3.单元测试框架的作用

单元测试框架不仅适用于单元测试,也适用于web自动化测试用例的开发和执行。它能更好地维护和管理测试用例,通过单元测试框架还能对测试结果统计和生成测试报告。

4.最基本的单元测试示例

下面以计算器类中的加减乘除的方法为被测试对象。

# 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

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 并提供测试结果

1.TestCase 基类(测试用例)

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))
(1) 一个完整的基本实例

以下是测试 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()这些,大致有三组:

一组用于运行测试的方法,另一组用于检查条件和报告故障的断言方法,还有一些收集测试本身信息的查询方法。

(2) TestCase 运行测试的方法
  • 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)主要是针对该测试类完成后再做的收尾工作,比如保存文件、关闭文件,断开数据库连接、保存生成测试报告、发送测试完成的通知邮件等。

(3) TestCase 的断言方法

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)断言对象不是另一个类的实例

实际环境示例

image-20200809214237463

其中 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
(4) TestCase 收集异常警告日志的方法

可用以下方法检查异常(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)
2.TestFixture 测试固件

一个测试代码的运行环境叫做 Testfixture 测试固件,代表执行多个测试所需的环境准备和清理动作。如创建临时或代理数据库、目录或启动服务器进程。一个新的 TestCase 的子类实例作为一个测试固件,用来运行每个独立的测试方法。在运行每个测试时,setUp() 、tearDown()__init__() 会被调用一次。以下是我写的一个关于测试固件结构的简单示例:

image-20200809220722861

3.TestSuite 测试集(测试套件)

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

实际环境示例

image-20200809212657577

4.TestLoader.discover 探索性测试

探索性测试在 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)

实际环境示例

image-20200809212858968

以下的装饰器可以用来装饰想跳过不执行的测试方法

@unittest.skip()			#装饰一个测试方法,表示直接跳过该方法,不执行该方法。
@unittest.skipIf(条件)	   #满足条件才跳过该测试方法
@unittest.skipUnless(条件)   #不满足条件才跳过该测试方法
@unittest.expectedFailure   #装饰一个测试方法,表示把测试标记为预计失败。

实际环境示例,测试结果显示 skip 了指定 test case
image-20200810215252951

5.TestRunner 测试运行器

实例化 unittest.TextTestRunner 对象,得到的这个运行器用于执行和输出测试结果,可能使用图形、文本接口,或返回一个特定值表示运行测试的结果。

if __name__ == '__main__':

    myRunner = unittest.TextTestRunner() # 创建一个 unittest.TextTestRunner 测试运行器对象
    myRunner.run(mySuite())              # 调用 myRunner 的 run 方法

三、博主原创手动码字,喜欢就点赞收藏

博主CSDN唯一首发,原创手动码字,喜欢就点赞收藏 😉

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值