当前位置: 首页 > 工具软件 > PyUnit > 使用案例 >

python 单元测试——PyUnit

史飞尘
2023-12-01

PyUnit(unittest) 是 python的单元测试框架,可基于PyUnit编写和运行可重复执行的单元测试;

PyUnit 是 xUnit 体系的一个成员,xUnit 是众多测试框架的统统称(cppUnit、jUnit等),PyUnit 主要用于进行白盒测试和回归测试。

使用 PyUnit 具有如下好处:

  • 测试代码与产品代码分离;同步编写测试代码和产品代码;
  • 易于编写单元测试代码、功能测试代码。
  • PyUnit 开源,可以基于PyUnit做二次开发,定制自己的PyUnit。


PyUnit 具有如下三个主要特征:

  • 使用断言方法判断期望值和实际值的差异,返回 bool 值。
  • 测试驱动设备可使用共同的初始化变量或实例。
  • 测试包结构便于组织和集成运行。

PyUnit (unittest) 的用法

更多详细的用法见:https://docs.python.org/3/library/unittest.html

所有测试的本质其实都是一样的,都是通过给定参数来执行函数,然后判断函数的实际输出结果和期望输出结果是否一致。

目前还有一种流行的开发方式叫作测试驱动开发,这种方式强调先编写测试用例,然后再编写函数和方法。假如程序要开发满足 A 功能的 fun_a() 函数,采用测试驱动开发的步骤如下:

  1. 为 fun_a() 函数编写测试用例,根据业务要求,使用大量不同的参数组合来执行 fun_a() 函数,并断言该函数的执行结果与业务期望的执行结果匹配。
  2. 编写、修改 fun_a() 函数。
  3. 运行 fun_a() 函数的测试用例,如果测试用例不能完全通过;则重复第 2 步和第 3 步,直到 fun_a() 的所有测试用例全部通过。

测试驱动开发强调结果导向,也就是在开发某个功能之前,先定义好该功能的最终结果(测试用例关注函数的执行结果),然后再去开发该功能。就像建筑工人在砌墙之前,要先拉好一根笔直的绳子(作用相当于测试用例),然后再开始砌墙,这样砌出来的墙就会符合标准。所以说测试驱动开发确实是一种不错的开发方式。

下面开发一个简单的 funcation_demo.py 程序,该py文件包含几个常见的函数:加法、减法、乘法、除法、开方。

# -*- coding: utf-8 -*-
# @file   : function_demo.py
# @author : shlian
# @date   : 2019/8/22
# @version: 1.0
# @desc   :
import math

def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def multi(a,b):
    return a*b

def div(a,b):
    if b==0:
        raise ValueError("invalid value:0")
    return a/b

def sqrt(a):
    if a<0:
        raise ValueError("invalid value:{0}".format(a))
    else:
        return math.sqrt(a)

在定义好上面的 function_demo.py 程序之后,该程序就相当于一个模块,接下来为该模块编写单元测试代码。

unittest 要求单元测试类必须继承 unittest.TestCase,该类中的测试方法需要满足如下要求:

  • 测试方法应该没有返回值。
  • 测试方法不应该有任何参数。
  • 测试方法应以test 开头。


下面是测试用例的代码:

# -*- coding: utf-8 -*-
# @file   : test_function_demo.py
# @author : shlian
# @date   : 2019/8/22
# @version: 1.0
# @desc   :
from function import function_demo
import unittest
import pysnooper

class test_function_demo(unittest.TestCase):

    def setUp(self):
        print("___________________________________init unit test____________________________________________________")

    def tearDown(self):
        print("___________________________________done______________________________________________________________")

    @pysnooper.snoop()
    def test_add_init(self):
        self.assertEqual(first=3,second=function_demo.add(1,2))

    @pysnooper.snoop()
    def test_add_float(self):
        self.assertEqual(first=3.789,second=function_demo.add(3,0.789))

    @pysnooper.snoop()
    def test_add_string(self):
        self.assertEqual(first="abcdefg",second=function_demo.add("ab","cdefg"))
#********************************************************************************************************************
    @pysnooper.snoop()
    def test_sub_int(self):
        self.assertTrue(5==function_demo.sub(105,100))

    @pysnooper.snoop()
    def test_sub_float(self):
        self.assertNotEqual(first=5.78,second=function_demo.sub(10.79,5.01))#这里不等,思考一下为什么呢?

    @pysnooper.snoop()
    def test_sub_float2(self):
        self.assertAlmostEqual(first=5.78,second=function_demo.sub(10.79,5.01),places=2)
#*********************************************************************************************************************
    @pysnooper.snoop()
    def test_multi_number(self):
        self.assertEqual(first=24,second=function_demo.multi(3,8))

    @pysnooper.snoop()
    def test_multi_list(self):
        self.assertListEqual(list1=['th','th','th','th','th','th'],list2=function_demo.multi(6,['th']))

    @pysnooper.snoop()
    def test_multi_str(self):
        self.assertMultiLineEqual(first="aaaaaaaaaaaaaaaaa",second=function_demo.multi(17,"a"))
        self.assertFalse("0123456789"==function_demo.multi(8,"0"))
#*********************************************************************************************************************
    @pysnooper.snoop()
    def test_div_int(self):
        self.assertEqual(first=5,second=function_demo.div(100,20))

    @pysnooper.snoop()
    def test_div_float(self):
        self.assertNotEqual(first=0.33333,second=function_demo.div(1,3))

    @pysnooper.snoop()
    def test_div_float2(self):
        self.assertAlmostEqual(first=0.33333,second=function_demo.div(1,3),places=5)

    @pysnooper.snoop()
    def test_div_exception(self):
        with self.assertRaises(Exception):
            function_demo.div(1,0)


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

上面测试用例中使用断言方法判断函数的实际输出结果与期望输出结果是否一致,如果一致则表明测试通过,否则表明测试失败。

在测试某个方法时,如果实际测试要求达到某种覆盖程度,那么在编写测试用例时必须传入多组参数来进行测试,使得测试用例能达到指定的逻辑覆盖。

unittest.TestCase 内置了大量 assertXxx 方法来执行断言,其中最常用的断言方法如表 1 所示。
 

表 1 TestCase 中最常用的断言方法
断言方法检查条件
assertEqual(a, b)a == b
assertNotEqual(a, b)a != b
assertTrue(x)bool(x) is True
assertFalse(x)bool(x) is False
assertIs(a, b)a is b
assertIsNot(a, b)a is not b
assertIsNone(x)x is None
assertIsNotNone(x)x is not None
assertIn(a, b)a in b
assertNotIn(a, b)a not in b
assertlsInstance(a, b)isinstance(a, b)
assertNotIsInstance(a, b)not isinstance(a, b)


除了上面这些断言方法,如果程序要对异常、错误、警告和日志进行断言判断,TestCase 提供了如表 2 所示的断言方法。
 

表 2 TestCase 包含的与异常、错误、警告和日志相关的断言方法
断言方法检查条件
assertRaises(exc, fun, *args, **kwds)fun(*args, **kwds) 引发 exc 异常
assertRaisesRegex(exc, r, fun, *args, **kwds)fun(*args, **kwds) 引发 exc 异常,且异常信息匹配 r 正则表达式
assertWarns(warn, fun, *args, **kwds)fun(*args, **kwds) 引发 warn 警告
assertWamsRegex(warn, r, fun, *args, **kwds)fun(*args, **kwds) 引发 warn 警告,且警告信息匹配 r 正则表达式
assertLogs(logger, level)With 语句块使用日志器生成 level 级别的日志


TestCase 还包含了如表 3 所示的断言方法用于完成某种特定检查。
 

表 3 TestCase 包含的用于完成某种特定检查的断言方法
断言方法检查条件
assertAlmostEqual(a, b)round(a-b, 7) == 0
assertNotAlmostEqual(a, b)round(a-b, 7) != 0
assertGreater(a, b)a > b
assertGreaterEqual(a, b)a >= b
assertLess(a, b)a < b
assertLessEqual(a, b)a <= b
assertRegex(s, r)r.search(s)
assertNotRegex(s, r)not r.search(s)
assertCountEqual(a, b)a、b 两个序列包含的元素相同,不管元素出现的顺序如何


当测试用例使用 assertEqual() 判断两个对象是否相等时,如果被判断的类型是字符串、序列、列表、元组、集合、字典,则程序会自动改为使用如表 4 所示的断言方法进行判断。换而言之,如表 4 所示的断言方法其实没有必要使用,unittest 模块会自动应用它们。
 

表 4 TestCase 包含的针对特定类型的断言方法
断言方法用于比较的类型
assertMultiLineEqual(a, b)字符串(string)
assertSequenceEqual(a, b)序列(sequence)
assertListEqual(a, b)列表(list)
assertTupleEqual(a, b)元组(tuple)
assertSetEqual(a, b)集合(set 或 frozenset)
assertDictEqual(a, b)字典(dict)

运行测试

在编写完测试用例之后,可以使用如下两种方式来运行它们:

  1. 通过代码调用测试用例。程序可以通过调用 unittest.main() 来运行当前源文件中的所有测试用例。例如,在上面的测试用例中增加如下代码:

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

  2. 使用 unittest 模块运行测试用例。使用该模块的语法格式如下:

    python -m unittest 测试文件

    在使用 python -m unittest 命令运行测试用例时,如果没有指定测试用例,该命令将自动查找并运行当前目录下的所有测试用例。因此,程序也可直接使用如下命令来运行所有测试用例:

    py -m unittest


采用上面任意一种方式来运行测试用例,均可以看到如下输出结果:

___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_add_float>
09:57:23.011081 call        24     def test_add_float(self):
09:57:23.011081 line        25         self.assertEqual(first=3.789,second=function_demo.add(3,0.789))
09:57:23.011081 return      25         self.assertEqual(first=3.789,second=function_demo.add(3,0.789))
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_add_init>
09:57:23.013117 call        20     def test_add_init(self):
09:57:23.013117 line        21         self.assertEqual(first=3,second=function_demo.add(1,2))
09:57:23.013117 return      21         self.assertEqual(first=3,second=function_demo.add(1,2))
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_add_string>
09:57:23.014073 call        28     def test_add_string(self):
09:57:23.014073 line        29         self.assertEqual(first="abcdefg",second=function_demo.add("ab","cdefg"))
09:57:23.015099 return      29         self.assertEqual(first="abcdefg",second=function_demo.add("ab","cdefg"))
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_div_exception>
09:57:23.015099 call        69     def test_div_exception(self):
09:57:23.015099 line        70         with self.assertRaises(Exception):
09:57:23.015099 line        71             function_demo.div(1,0)
09:57:23.016067 exception   71             function_demo.div(1,0)
ValueError: invalid value:0
09:57:23.016067 return      71             function_demo.div(1,0)
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_div_float>
09:57:23.017065 call        61     def test_div_float(self):
09:57:23.017065 line        62         self.assertNotEqual(first=0.33333,second=function_demo.div(1,3))
09:57:23.017065 return      62         self.assertNotEqual(first=0.33333,second=function_demo.div(1,3))
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_div_float2>
09:57:23.018091 call        65     def test_div_float2(self):
09:57:23.018091 line        66         self.assertAlmostEqual(first=0.33333,second=function_demo.div(1,3),places=5)
09:57:23.018091 return      66         self.assertAlmostEqual(first=0.33333,second=function_demo.div(1,3),places=5)
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_div_int>
09:57:23.019060 call        57     def test_div_int(self):
09:57:23.020098 line        58         self.assertEqual(first=5,second=function_demo.div(100,20))
09:57:23.020098 return      58         self.assertEqual(first=5,second=function_demo.div(100,20))
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_multi_list>
09:57:23.021054 call        48     def test_multi_list(self):
09:57:23.021054 line        49         self.assertListEqual(list1=['th','th','th','th','th','th'],list2=function_demo.multi(6,['th']))
09:57:23.021054 return      49         self.assertListEqual(list1=['th','th','th','th','th','th'],list2=function_demo.multi(6,['th']))
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_multi_number>
09:57:23.022052 call        44     def test_multi_number(self):
09:57:23.022052 line        45         self.assertEqual(first=24,second=function_demo.multi(3,8))
09:57:23.022052 return      45         self.assertEqual(first=24,second=function_demo.multi(3,8))
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_multi_str>
09:57:23.024046 call        52     def test_multi_str(self):
09:57:23.024046 line        53         self.assertMultiLineEqual(first="aaaaaaaaaaaaaaaaa",second=function_demo.multi(17,"a"))
09:57:23.024046 line        54         self.assertFalse("0123456789"==function_demo.multi(8,"0"))
09:57:23.024046 return      54         self.assertFalse("0123456789"==function_demo.multi(8,"0"))
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_sub_float>
09:57:23.025043 call        36     def test_sub_float(self):
09:57:23.026040 line        37         self.assertNotEqual(first=5.78,second=function_demo.sub(10.79,5.01))#这里不等,思考一下为什么呢?
09:57:23.026040 return      37         self.assertNotEqual(first=5.78,second=function_demo.sub(10.79,5.01))#这里不等,思考一下为什么呢?
Return value:.. None
___________________________________done______________________________________________________________
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_sub_float2>
09:57:23.026040 call        40     def test_sub_float2(self):
09:57:23.026040 line        41         self.assertAlmostEqual(first=5.78,second=function_demo.sub(10.79,5.01),places=2)
09:57:23.027037 return      41         self.assertAlmostEqual(first=5.78,second=function_demo.sub(10.79,5.01),places=2)
Return value:.. None
___________________________________done______________________________________________________________


Ran 13 tests in 0.018s

OK
___________________________________init unit test____________________________________________________
Source path:... D:\shlian\github\python\project\unit_test\unit_tests\test_function_demo.py
Starting var:.. self = <test_function_demo.test_function_demo testMethod=test_sub_int>
09:57:23.028035 call        32     def test_sub_int(self):
09:57:23.028035 line        33         self.assertTrue(5==function_demo.sub(105,100))
09:57:23.028035 return      33         self.assertTrue(5==function_demo.sub(105,100))
Return value:.. None
___________________________________done______________________________________________________________

Process finished with exit code 0

这里的每个点都代表一个测试用例(每个以 test_ 开头的方法都是一个真正独立的测试用例)的结果。由于上面测试类中包含了两个测试用例,因此此处看到两个点,其中点代表测试用例通过。此处可能出现如下字符:

  • .:代表测试通过。
  • F:代表测试失败,F 代表 failure。
  • E:代表测试出错,E 代表 error。
  • s:代表跳过该测试,s 代表 skip。
 类似资料: