Python单元测试框架——unittest(二)


断言

每个测试用例都要进行断言,如果没有断言或没有进行正确断言,那这条测试用例是无效的。(没有断言相当于功能测试中测试用例没有写预期结果;没有写正确的断言,相当于功能测试中测试用例预期结果写错了。)

unittestTestCase类提供了一些断言方法,如下:

方法 检查对象 功能描述
assertEqual(a,b) a == b 测试a和b是否相等。 如果两个值的比较结果是不相等,则测试将失败。
assertNotEqual(a,b) a != b 测试a和b是否不等。 如果两个值的比较结果是相等,则测试将失败。
assertTrue(x) bool(x) is True 测试x是否为真
assertFalse(x) bool(x) is False 测试x是否为假
assertIs(a,b) a is b 测试a和b是同一个对象
assertIsNot(a,b) a is not b 测试a和b不是同一个对象
assertIsNone(x) x is None 测试x是None
assertIsNotNone(x) x is not None 测试x不是None
assertIn(a,b) a in b 测试a是b的成员
assertNotIn(a,b) a not in b 测试a不是b的成员
assertIsInstance(a,b) isinstance(a,b) 测试a(对象)是b(类)的实例
assertNotIsInstance(a,b) not isinstance(a,b) 测试a(对象)不是b(类)的实例

上面12个断言中,最常用的是下面3个:

  • assertEqual(a,b)

    import unittest
    
    class PlsRegister(unittest.TestCase):
    
        # 测试用例01-测试注册账号-老百
        @unittest.skip("老百是系统初始化账号无需注册")
        def test_01_laobai(self):
            # self.skipTest("跳过此用例。。。")
            print("测试注册账号-老百")
    
        # 测试用例02-测试注册账号-nichengwe
        def test_02_nichengwe(self):
            print("测试注册账号-nichengwe")
            self.assertEqual(1, 1)
    
        # 测试用例03-测试注册账号-baby2016
        def test_03_baby2016(self):
            print("测试注册账号-baby2016")
            self.assertEqual(1, 11)

    执行结果:

    $ python -m unittest test_pls_register -v
    test_01_laobai (test_pls_register.PlsRegister) ... skipped '老百是系统初始化账号无需注册'
    test_02_nichengwe (test_pls_register.PlsRegister) ... 测试注册账号-nichengwe
    ok
    test_03_baby2016 (test_pls_register.PlsRegister) ... 测试注册账号-baby2016
    FAIL
    
    ======================================================================
    FAIL: test_03_baby2016 (test_pls_register.PlsRegister)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/laobai/workspaces/testdemo01/unittestdemo/test_pls_register.py", line 29, in test_03_baby2016
        self.assertEqual(1, 11)
    AssertionError: 1 != 11
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    FAILED (failures=1, skipped=1)
  • assertTrue(x)

    import unittest
    
    class PlsRegister(unittest.TestCase):
        # age = 17
    
        # 测试用例01-测试注册账号-老百
        @unittest.skip("老百是系统初始化账号无需注册")
        def test_01_laobai(self):
            # self.skipTest("跳过此用例。。。")
            print("测试注册账号-老百")
    
        # 测试用例02-测试注册账号-nichengwe
        def test_02_nichengwe(self):
            print("测试注册账号-nichengwe")
            self.assertTrue(1)
    
        # 测试用例03-测试注册账号-baby2016
        def test_03_baby2016(self):
            print("测试注册账号-baby2016")
            self.assertTrue(0)

    执行结果:

    $ python -m unittest test_pls_register -v
    test_01_laobai (test_pls_register.PlsRegister) ... skipped '老百是系统初始化账号无需注册'
    test_02_nichengwe (test_pls_register.PlsRegister) ... 测试注册账号-nichengwe
    ok
    test_03_baby2016 (test_pls_register.PlsRegister) ... 测试注册账号-baby2016
    FAIL
    
    ======================================================================
    FAIL: test_03_baby2016 (test_pls_register.PlsRegister)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/laobai/workspaces/testdemo01/unittestdemo/test_pls_register.py", line 29, in test_03_baby2016
        self.assertTrue(0)
    AssertionError: 0 is not true
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    FAILED (failures=1, skipped=1)
  • assertIn(a,b)

    import unittest
    
    class PlsRegister(unittest.TestCase):
        # age = 17
    
        # 测试用例01-测试注册账号-老百
        @unittest.skip("老百是系统初始化账号无需注册")
        def test_01_laobai(self):
            # self.skipTest("跳过此用例。。。")
            print("测试注册账号-老百")
    
        # 测试用例02-测试注册账号-nichengwe
        def test_02_nichengwe(self):
            print("测试注册账号-nichengwe")
            self.assertIn("h", "hello")
    
        # 测试用例03-测试注册账号-baby2016
        def test_03_baby2016(self):
            print("测试注册账号-baby2016")
            self.assertIn("a", "hello")

    执行结果:

    $ python -m unittest test_pls_register -v
    test_01_laobai (test_pls_register.PlsRegister) ... skipped '老百是系统初始化账号无需注册'
    test_02_nichengwe (test_pls_register.PlsRegister) ... 测试注册账号-nichengwe
    ok
    test_03_baby2016 (test_pls_register.PlsRegister) ... 测试注册账号-baby2016
    FAIL
    
    ======================================================================
    FAIL: test_03_baby2016 (test_pls_register.PlsRegister)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/laobai/workspaces/testdemo01/unittestdemo/test_pls_register.py", line 29, in test_03_baby2016
        self.assertIn("a", "hello")
    AssertionError: 'a' not found in 'hello'
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    FAILED (failures=1, skipped=1)

测试报告

上面描述的一系列测试过程结束后,只是在控制台输出了简单的几行结果描述,而测试报告作为测试活动的一个重要的结果物,需要在测试完成后,出具一份有详细说明的报告。

HTMLTestRunner

HTMLTestRunner是最古老的一款unittest的HTML格式的报告,这个报告仅支持python2(python2已停止维护多年),地址:http://tungwaiyip.info/software/HTMLTestRunner.html

下载HTMLTestRunner.py 后,需要稍作改动,以支持python3。

# 94行,import StringI0 改为: import io
# 539行,self.outputBuffer = StringI0.StringIO() 改为: self.outputBuffer = io.BytesIO()
# 631行,print >>sys .stderr, '\nTime ELapsed: %s' % (self.stopTime-seLf.startTime) 改为: print('\nTime Elapsed: %s' % (self.stopTime-self.startTime))
# 642行,if not rmap.has_key(cls): 改为: if not cls in rmap:
# 766行,uo = o.decode('latin-1') 改为: uo = e
# 772行,ue = e.decode('latin-1') 改为: ue = e

将修改后的HTMLTestRunner.py拷贝到项目的/venv/lib/python3.9/site-packages/目录下

HTMLTestRunner方法有四个参数:HTMLTestRunner(stream='', verbosity='', title='', description=')

  • stream是报告输出的路径+文件名.html;
  • verbosity是输出信息的详细程序,由于是输出到html的报告,这个参数可以省略;
  • title是测试报告的名称;
  • description是测试报告的具体描述;
import unittest
from HTMLTestRunner import HTMLTestRunner


class PlsRegister(unittest.TestCase):

    # 测试用例01-测试注册账号-老百
    @unittest.skip("老百是系统初始化账号无需注册")
    def test_01_laobai(self):
        # self.skipTest("跳过此用例。。。")
        print("测试注册账号-老百")

    # 测试用例02-测试注册账号-nichengwe
    def test_02_nichengwe(self):
        print("测试注册账号-nichengwe")
        self.assertIn("h", "hello")

    # 测试用例03-测试注册账号-baby2016
    def test_03_baby2016(self):
        print("测试注册账号-baby2016")
        self.assertIn("a", "hello")


if __name__ == '__main__':
    suite = unittest.TestSuite()
    testcases = [PlsRegister('test_02_nichengwe'), PlsRegister('test_01_laobai'), PlsRegister('test_03_baby2016')]
    suite.addTests(testcases)
    fp = open('./result.html', 'wb')
    runner = HTMLTestRunner(stream=fp, title='PLS自动化测试报告', description='用例执行情况:')
    runner.run(suite)
    fp.close()

输出的报告如下图所示:

注:由于HTMLTestRunner测试报告过于简陋,所以基本不建议使用。

unittestreport

unittestreport是由国内培训机构的成员设计的一款开源基于unittest的测试报告模板。

github地址:https://github.com/musen123/UnitTestReport

a. 安装

pip install unittestreport

b. 使用

  1. 加载测试套件;
  2. 使用unittestteport中的TestRunner创建一个运行对象;
  3. 执行测试;
import unittest
import unittestreport

# 1、加载测试用例到套件中
suite = unittest.defaultTestLoader.discover('/Users/laobai/workspaces/testdemo01/unittestdemo')

# 2、创建一个用例运行程序
runner = unittestreport.TestRunner(suite,
                                   tester='老百',
                                   filename="report.html",
                                   report_dir=".",
                                   title='PLS自动化测试报告',
                                   desc='用例执行情况',
                                   templates=1
                                   )

# 3、运行测试用例
runner.run()

其中templates目前有三个,用1,2,3表示,这个示例中用的是1。生成的报告如下:

其中,用例描述,是指每个测试用例(方法)中的文档注释"""测试注册账号-老百"""

...
    # 测试用例01-测试注册账号-老百
    @unittest.skip("老百是系统初始化账号无需注册")
    def test_01_laobai(self):
        '''测试注册账号-老百'''
        print("测试注册账号-老百")
...

https://unittestreport.readthedocs.io/en/latest/ ,这是unittestreport的在线帮助文档,介绍了比较多的一些使用细节、技巧。

(类似的测试报告模板很不少,这个仅是其中的一款。由于在之后的项目中基本使用pytest来替代unittest,所以够用就好)

参数化

Python标准库中的unittest自身不支持参数化测试,为了解决这个问题,可以使用第三方库来实现,主要有两个:DDTparameterized

DDT

DDTData-Driven Test(数据驱动测试),在同一个方法上测试不同的参数,以覆盖所有可能的预期分支的结果。它的测试数据可以与测试行为分离,被放入到文件、数据库或者外部介质中,再由测试程序读取。

DDT是通过装饰器的形式来调用的,主要使用下面4个装饰器:

@ddt (类装饰器,申明当前类使用ddt框架)

@data (函数装饰器,用于给测试用例传递数据)

@unpack (函数装饰器,将传输的数据包解包(一般作用于元组tuple和列表list))

@file_data (函数装饰器,可直接读取yaml/json文件)

a. 安装ddt

$ pip install ddt

b. 示例1

参数化前的代码

import unittest


class PlsLogin(unittest.TestCase):

    # 测试用例01-测试老百登录
    def test_01_laobai(self):
        print("测试老百登录")

    # 测试用例02-测试nichengwe登录
    def test_02_nichengwe(self):
        print("测试nichengwe登录")

    # 测试用例02-测试baby2016登录
    def test_03_baby2016(self):
        print("测试baby2016登录")

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

执行结果:

$ python pls_login_01.py 
测试老百登录
.测试nichengwe登录
.测试baby2016登录
.
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

通过ddt来进行参数化

import unittest
from ddt import ddt, data

# 使用@ddt声明这个类为ddt修饰的
@ddt
class PlsLogin(unittest.TestCase):

		# ddt所使用的数据
    @data('laobai','nichengwe','baby2016')
    def test_login(self,login_name):
        print("测试%s登录"%login_name)
        
if __name__ == '__main__':
    unittest.main()

执行结果:

$ python pls_login_01.py 
测试laobai登录
.测试nichengwe登录
.测试baby2016登录
.
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

@data传入多个值的时候,传几个值,用例就执行几次。

c. @data的参数可以是数字、字符串、列表、元组、字典、集合

@data(10,20)
@data(['laobai','nichengwe','baby2016'])
@data({'name':'laobai'},{'password':'123456'})

其中:

  • 如果是数字或字符串,不需要使用@unpack解包;
  • 如果是元组和列表的话元组和列表,可以通过@unpack进行解包,参数的个数必须和解完包的值的个数一致;
  • 如果是字典,可以通过@unpack进行解包,参数的名字和个数必须和字典的键保持一致;
  • 如果是集合,无法进行解包;

d. 示例2

示例1中,传给data的值为多组单个值,若需要传递多组多个值,可以使用@unpack装饰器

import unittest
from ddt import ddt, data, unpack

@ddt
class PlsLogin(unittest.TestCase):

    @data(('laobai', '123456'),('nichengwe','654321'))
    @unpack
    def test_login(self, login_name, password):
        print("测试%s登录,使用密码为%s" % (login_name,password))


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

执行结果:

$ python pls_login_01.py 
测试laobai登录,使用密码为123456
.测试nichengwe登录,使用密码为654321
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

测试数据为多组字典形式

import unittest
from ddt import ddt, data, unpack

@ddt
class PlsLogin(unittest.TestCase):

    @data({'login_name': 'laobai', 'password': '123456'}, {'login_name': 'nichengwe', 'password': '654321'})
    @unpack
    def test_login(self, login_name, password):
        print("测试%s登录,使用密码为%s" % (login_name, password))


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

执行j结果:

$ python pls_login_01.py 
测试laobai登录,使用密码为123456
.测试nichengwe登录,使用密码为654321
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

e. 示例3

import unittest
from ddt import ddt, data, unpack


@ddt
class TestDemo01(unittest.TestCase):

    @data([1, 2, 3], [2, 3, 5], [3, 4, 7])
    @unpack
    def test_add(self, data1, data2, exceptdata):
        sum = data1 + data2
        self.assertEqual(sum, exceptdata)


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

执行结果:

$ python test_demo01.py -v
test_add_1__1__2__3_ (__main__.TestDemo01) ... ok
test_add_2__2__3__5_ (__main__.TestDemo01) ... ok
test_add_3__3__4__7_ (__main__.TestDemo01) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

上面的多个例子中的数据,都是写在程序的py文件中,相当于写死了测试数据。这里可以使用ddtfile_data读取外部文件的方式来获取测试数据

import unittest
from ddt import ddt, data, unpack, file_data


@ddt
class TestDemo01(unittest.TestCase):

    @file_data('data.json')
    @unpack
    def test_add(self, value):
        data1, data2, exceptdata = value.split('|')
        sum = int(data1) + int(data2)
        self.assertEqual(sum, int(exceptdata))


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

执行结果:

$ python test_demo01.py -v
test_add_1_1_2_3 (__main__.TestDemo01)
test_add_1_1_2_3 ... ok
test_add_2_2_3_5 (__main__.TestDemo01)
test_add_2_2_3_5 ... ok
test_add_3_3_4_7 (__main__.TestDemo01)
test_add_3_3_4_7 ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Parameterized

Parameterized是Python的一个参数化库,使用时先导入Parameterized库下面的parameterized类。再通过@parameterized.expand()来装饰测试用例的方法即可。

a. 安装parameterized

$ pip install parameterized

b. 示例

import unittest
from parameterized import parameterized


class TestDemo02(unittest.TestCase):

    @parameterized.expand([(3, 1), (10, 5), (1.1, 1.0)])
    def test_values(self, first, second):
        self.assertTrue(first > second)


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

执行结果:

$ python test_demo02.py -v
test_values_0 (__main__.TestDemo02) ... ok
test_values_1 (__main__.TestDemo02) ... ok
test_values_2 (__main__.TestDemo02) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

DDT的参数化能力要强于Parameterized,所以更建议使用DDT


文章作者: 老百
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 老百 !
 上一篇
Python单元测试框架——pytest(一) Python单元测试框架——pytest(一)
pytest是一个功能齐全的Python单元测试框架,可以帮助编写更好的程序,不仅可以编写小测试,还可以扩展到复杂的功能测试。
2022-12-03
下一篇 
Python单元测试框架——unittest(一) Python单元测试框架——unittest(一)
unittest单元测试框架是受到JUnit的启发,与其他语言中的主流单元测试框架有着相似的风格。其支持测试自动化,配置共享和关机代码测试。支持将测试样例聚合到测试集中,并将测试与报告框架独立。
2022-11-19
  目录