unittest测试框架

一、介绍

  • untitest框架是专门用来进行执行代码测试的框架
  • untitest是python自带的单元测试包,被各种框架引用,比如django
  • unittest很多用法和django自带的单元测试差不多,因为Django的TestCase继承了Python的unittest.TestCase

特点:

  1. 能够组织多个用例去执行
  2. 提供丰富的断言方法
  3. 提供丰富的日志与测试结果

核心要素:

  1. TestCase
  2. TestSuite
  3. TextTestRunner
  4. Fixture
  5. defaultTestLoader

1、TestCase

说明:一个TestCase就是一条测试用例,就是一个完整的测试流程

  1. 新建类继承自 unittest.TestCase
  2. 测试的方法以 test 开头

2、TestSuite

说明:测试套件是把多条测试用例集合在一起,就是一个TestSuite

  1. 实例化 suite 对象 suite = unittest.TestSuite()
  2. 将测试用例添加到 suite 中 suite.addTest(MyTest('test_xxx'))

3、TestTestRunner

说明:测试执行是用来执行测试用例套件

  1. 实例化 runner 对象 runner = unittest.TextTestRunner()
  2. 执行测试套件 runner.run(suite)

案例

from unittest import TestCase
from unittest import TestCase
from unittest import TestSuite, TextTestRunner


class TestCase01(TestCase):
    def test_01(self):
        print("01 执行")

    def test_02(self):
        print("02 执行")

    def test_03(self):
        print("03 执行")


if __name__ == '__main__':
    suite = TestSuite()
    # 添加要执行的测试用例
    suite.addTest(TestCase01('test_01'))
    suite.addTest(TestCase01('test_02'))
    suite.addTest(TestCase01('test_03'))

    # 创建runner
    runner = TextTestRunner()
    runner.run(suite)
View Code

运行结果

$ python test_1_case.py
01 执行
.02 执行
.03 执行
.
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

4、Fixture

说明:是一个概述,对一个测试用例环境的搭建和销毁就是一个Fixture;

对应到代码就是我们前面所学过的 setup 和 teardown 的作用

5、defaultTestLoader(掌握)

说明: unittest.defaultTestLoader.discover 方法可以自动把指定目录下测试用例自动添加到测试套件中,避免手动一个个的添加测试用例。这个方法返回的就是 suite 对象。

  1. 指定要搜索的目录
  2. 调用 discover 方法生成 suite 对象
import unittest

if __name__ == '__main__':
    # 参数1:指定搜索的路径
    # 参数2:指定匹配文件,默认为'test*.py'
    suite = unittest.defaultTestLoader.discover('./', 'test_2_fixture.py')

    runner = unittest.TextTestRunner()

    runner.run(suite)
View Code

二、使用

使用unittest编写python的单元测试代码,包括如下几个步骤:

  1. 导入unittest模块
  2. 定义一个继承自unittest.TestCase的测试用例类,如class xxx(unittest.TestCase):
  3. 定义setUp和tearDown,如果定义了则会在每个测试case执行前先执行setUp方法,执行完毕后执行tearDown方法。
  4. 定义测试用例,名字以test开头,unittest会自动将test开头的方法放入测试用例集中。
  5. 一个测试用例应该只测试一个方面,测试目的和测试内容应很明确。主要是调用assertEqual、assertRaises等断言方法判断程序执行结果和预期值是否相符。
  6. 调用unittest.main()启动测试,或实例化runner对象执行测试集、测试函数
  7. 如果测试未通过,则会显示e,并给出具体的错误(此处为程序问题导致)。如果测试失败则显示为f,测试通过为.,如有多个testcase,则结果依次显示。

实例代码

import unittest


class TestAdd(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """setUpClass为类的初始化方法,在整个类运行前执行只执行一次"""
        print('setUpClass')

    def setUp(self):
        """为测试方法的初始化,每个text函数运行前执行一次"""
        self.a = 20
        self.b = 10
        print('setUp')

    def tearDown(self):
        """清理函数,和setUp类似,每个text函数执行后执行一次"""
        print('tearDown')

    @classmethod
    def tearDownClass(cls):
        """和setUpclass类似,在调用整个类测试方法完成后执行一次"""
        print('tearDownClass')

    def test_add(self):
        """验证加法"""
        result = self.a + self.b
        self.assertEqual(result, 30)
        print('test_add')

    def test_sub(self):
        """验证减法"""
        result = self.a - self.b
        self.assertEqual(result, 10)
        print('test_sub')


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

注意

  • 在pycharm中右键运行的位置不同,结果也不同;要完整运行,鼠标放在最后一行代码的位置,再右键运行,也可以用命令行:python xxx.py运行 
  • unittest默认加载脚本的顺序是:根据ASCII码的顺序加载,数字与字母的顺序为:0-9,A-Z,a-z

三、断言

1、介绍

  • 断言可以简单理解为:对预期和实际的结果进行比对,比对的结果是真或假,真表示成功,假表示失败。一般情况下,如果是失败的,还应该给出失败的原因。
  • Django 中的测试用例,是通过判断执行过程中有没有抛出异常来表示成功还是失败,抛出异常就表示失败。
  • 当断言失败时,会抛出AssertionError

2、为什么需要断言?

自动化脚本在执行的时候一般都是无人值守状态,我们不知道执行结果是否符合预期结果,所以我们需要让程序代替人为检测程序执行的结果是否符合预期结果,这就需要使用断言

3、unittest提供的断言函数

查表

最常用:assertEqual(arg1, arg2, msg=None)——验证arg1=argue2

四、unittest参数化

安装parameterized

pip install parameterized -i https://pypi.tuna.tsinghua.edu.cn/simple

实例代码

import unittest
from parameterized import parameterized # pip install parameterized


class TestParams(unittest.TestCase):
    # 列表套元组,有多少个元组就执行多少次测试用例
    @parameterized.expand([
        ('mike', '13344445555'),
        ('yoyo', '13655556666'),
        ('lily', '13966665555')
    ])
    def test_param(self, user, num):
        """参数化"""
        print(f'{user} ==> {num}')


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

说明:

  • @parameterized.expand([(), (), …]):列表套元组,有多少个元组就执行多少次测试用例

五、mock

1、介绍

场景:

  • 假设开发的项目叫a,里面包含了一个模块b,模块b中的一个函数c(也就是a.b.c)
  • 需要调用特定的服务器来得到一个JSON返回值,然后根据这个返回值来做处理。

假设abc代码如下:

  1 import requests
  2 
  3 def c(url):
  4     resp = requests.get(url)

由于网络限制或者其他原因,在测试环境下无法访问到该服务器,那如何测试该函数呢?

方法:

  1. 搭建一个测试的服务器
  2. 使用Mock模块

Mock模块

  1. python中一个用于支持单元测试的库
  2. 使用mock对象替代掉指定的python对象,以达到模拟对象的行为

2、使用

类:unittest.mock.Mock

class Mock(return_value=unittest.mock.DEFAULT, side_effect=None)

Mock 类对象,是可调用对象,如同函数。可以在调用 mock 对象时传递参数

  • return_value(重要)

return_value : 默认是 unittest.mock.DEFAULT 。调用 mock 时,获得到的返回值。

import unittest
import unittest.mock

class MyTest(unittest.TestCase):
    def test_return_value(self):
        # 实例化对象, 需要传参:return_value=数值
        mock_obj = unittest.mock.Mock(return_value=9999)

        # Mock 类对象,是可调用对象,如同函数
        # 调用方式:mock对象()
        num = mock_obj()
        print(num)
View Code
  • side_effect(了解)

默认值 None。

  • 可以是一个 BaseException 对象或者 BaseException 的子类。调用 mock 会抛处这个异常
  • 可以是一个可以迭代对象。调用 mock 时会遍历该对象,如果没有元素可遍历了,会抛出异常
  • 可以是一个函数。调用 mock 时,就会调用该函数,传递给 mock 的参数,也会传递给该函数
  • 如果传递了 side_effect 参数,那么 return_value 就会被 side_effect 替代
import unittest
import unittest.mock


class MyTest(unittest.TestCase):
    def test_err(self):
        # side_effect = 异常对象
        # mock就是一个异常对象
        mock_obj = unittest.mock.Mock(side_effect=BaseException('自定义异常'))
        mock_obj()  # 这里会抛出异常

    def test_list(self):
        # side_effect = 列表
        mock_obj = unittest.mock.Mock(side_effect=[1, 2, 3])
        # mock_obj()一次,取一个,如果取完再取下一个,出异常
        print(mock_obj())
        print(mock_obj())
        print(mock_obj())
        print(mock_obj())  # StopIteration

    def test_func(self):
        def func(a, b):
            return a + b

        # side_effect=函数名,是函数名,没有()
        # mock_obj就是这个函数
        mock_obj = unittest.mock.Mock(side_effect=func)
        print(mock_obj(1, 2))
        print(mock_obj())  # func() missing 2 required positional arguments: 'a' and 'b
View Code

限制模拟的范围

mock的一个副作用:当使用 mock 掉一个对象后,默认情况下,在后面执行的代码都会受到影响,比如其他的测试用例,其他的代码,只要是这些代码是在 mock 之后执行的都会受影响。

import unittest
import unittest.mock
import pay
import pay_status

class TestPay(unittest.TestCase):

    # 只要前面的测试用例 mock 掉一个对象后,默认情况下,在后面执行的代码都会受到影响
    # 注意:保证有mock对象替换的先执行
    def test_1_success(self):
        pay.pay_way = unittest.mock.Mock(return_value={"result": "success", "reason":"null"})

        # 调用支付状态函数
        ret = pay_status.pay_way_status()
        self.assertEqual(ret, '支付成功', '支付失败')

    def test_2_nomock(self):
        # 调用支付状态函数
        # pay.pay_way返回还是{"result": "success", "reason":"null"}
        ret = pay_status.pay_way_status()
        self.assertEqual(ret, '支付失败', '测试失败')

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

结果:

 python test_pay2.py
/home/python/code/mock_code/pay_code/pay_status.py: result={'result': 'success', 'reason': 'null'}
./home/python/code/mock_code/pay_code/pay_status.py: result={'result': 'success', 'reason': 'null'}
F
======================================================================
FAIL: test_2_nomock (__main__.TestPay)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_pay2.py", line 21, in test_2_nomock
    self.assertEqual(ret, '支付失败', '测试失败')
AssertionError: '支付成功' != '支付失败'
- 支付成功
+ 支付失败
 : 测试失败

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
View Code
  • mock.path

有时候我们需要限定 mock 只在特定范围内有效,那么就需要使用 mock.patchmock.patch()通过以字符串形式描述路径的参数,返回 mock 对象

  • 把 patch 当做装饰器,可以限定在特定方法内
  • 把 patch 当做上下文管理器,可以限定在特定上下文管理器范围内
  • patch 装饰器

patch 接受一个参数,表示要替换对象的完整模块路径,例如: @mock.patch('pay.pay_way'),被装饰的方法需要接受一个额外的参数,这个参数是一个 mock 对象,我们可以在方法对操作这个对象

from unittest import mock
import unittest
import pay_status


class TestPayStatues(unittest.TestCase):
    '''单元测试用例'''

    @mock.patch('pay.pay_way')
    # patch函数中的参数是 脚本启动位置的绝对路径 str
    # pay.pay_way函数被替换为mock_pay_way, 作为被装饰函数的参数传入
    def test_success(self, mock_pay_way):
        '''测试支付成功场景'''
        # mock一个支付成功的数据, pay.pay_way函数被替换
        mock_pay_way.return_value = {"result": "success", "reason":"null"}
        # 根据支付结果测试页面跳转
        statues = pay_status.pay_way_status()
        print(statues)
        self.assertEqual(statues, "支付成功")


    def test_userinfo_nomock(self):
        # 这里我们没有进行 mock,抛出异常
        statues = pay_status.pay_way_status()
        print('test_userinfo_nomock = ', statues)

if __name__ == "__main__":
    unittest.main()
View Code
  • patch 上下文管理器
with mock.patch('pay.pay_way') as mock_obj mock_obj 是一个 mock 对象
from unittest import mock
import unittest
import pay_status


class TestPayStatues(unittest.TestCase):
    '''单元测试用例'''

    def test_success(self):
        '''测试支付成功场景'''
        with mock.patch('pay.pay_way') as mock_obj:
            mock_obj.return_value = {"result": "success", "reason":"null"}
            # 根据支付结果测试页面跳转
            statues = pay_status.pay_way_status()
            print(statues)
            self.assertEqual(statues, "支付成功")

    def test_userinfo_nomock(self):
        # 这里我们没有进行 mock,抛出异常
        statues = pay_status.pay_way_status()
        print('test_userinfo_nomock = ', statues)

if __name__ == "__main__":
    unittest.main()
View Code
  • 类方法替换

mock.patch.object(类, '方法名')返回指定类的函数的mock对象

from unittest import mock
import unittest

class Pay(object):
    def pay_way(self):
        """假设这里是一个支付的功能,未开发完
        支付成功返回:{"result": "success", "reason":"null"}
        支付失败返回:{"result": "fail", "reason":"余额不足"}
        reason返回失败原因
        """
        raise NotImplementedError('代码还没有实现')

    def pay_way_status(self):
        """根据支付的结果success或fail,判断跳转到对应页面
        假设这里的功能已经开发完成"""

        # todo 此时pay_way()函数并未完成!你先假定他完成了
        result = self.pay_way()
        print(result)

        if result["result"] == "success":
            return "支付成功"
        if result["result"] == "fail":
            return "支付失败"

class TestPayStatues(unittest.TestCase):
    '''单元测试用例'''

    def test_success1(self):
        '''测试支付成功场景'''
        p = Pay() # 实例化对象
        p.pay_way = mock.Mock(return_value = {"result": "success", "reason":"null"})
        statues = p.pay_way_status()
        print(statues)
        self.assertEqual(statues, "支付成功")

    @mock.patch.object(Pay, 'pay_way')
    def test_success2(self, mock_obj):
        '''测试支付成功场景'''
        mock_obj.return_value = {"result": "success", "reason":"null"}
        # 根据支付结果测试页面跳转
        statues = Pay().pay_way_status()
        print(statues)
        self.assertEqual(statues, "支付成功")


    def test_success3(self):
        '''测试支付成功场景'''
        with mock.patch.object(Pay, 'pay_way') as mock_obj:
            mock_obj.return_value = {"result": "success", "reason":"null"}
            # 根据支付结果测试页面跳转
            statues = Pay().pay_way_status()
            print(statues)
            self.assertEqual(statues, "支付成功")

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

常用的方法和属性

mock 的对象拥有一些可以用于单元测试的检查方法,可以用来测试 mock 对象的调用情况

常用的检查方法:

检查方法 作用和返回值

calledproperty

是否被调用过, 返回布尔值

call_count 获取调用测试, 返回调用测试
call_args 最后一次调用时使用的参数, 未调用返回None
call_args_list 所有调用时使用的参数列表
assert_called 检查是否被调用过,如果没有被调用过,则会抛出 AssertionError 异常
assert_called_once() 确保调用过一次,如果没调用或多于一次,则抛出 AssertionError 异常
assert_not_called 确保没被调用过,否则抛出 AssertionError 异常
assert_called_with(*args, **kargs) 检查最后一次调用时使用的参数
  1 import unittest
  2 import unittest.mock
  3 
  4 
  5 class MockTest(unittest.TestCase):
  6     def test_return_value(self):
  7         mock = unittest.mock.Mock(return_value=1999)
  8         result = mock()
  9         print(result)  # 打印 1999
 10 
 11         print(mock.called)  # 是否被调用过, 返回布尔值
 12         print(mock.call_count)  # 获取调用测试, 返回调用测试
View Code

六、测试报告

unittest的测试报告有很多模板或插件,这里介绍HTMLTestRunner和BeautifulReport两种插件

1、HTMLTestRunner

  • 安装HTMLTestRunner插件
  1 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple htmltestrunner-python3
  • 实例代码:
  1 """
  2 0. 导包, import unittest
  3 1. 定义类,继承unittest.TestCase
  4 2. 是一个fixture, 有前置后置方法
  5 3. 有test开头的测试用例,结果用断言判断
  6 4. 运行测试
  7 """
  8 import unittest
  9 from HTMLTestRunner.HTMLTestRunner import HTMLTestRunner
 10 
 11 
 12 class MyTest(unittest.TestCase):
 13     def setUp(self) -> None:
 14         print('setUp')
 15 
 16     def tearDown(self) -> None:
 17         print("tearDown")
 18 
 19     @classmethod
 20     def setUpClass(cls) -> None:
 21         print('setUpClass')
 22 
 23     @classmethod
 24     def tearDownClass(cls) -> None:
 25         print('tearDownClass')
 26 
 27     def test_1_add(self):
 28         num = 1 + 2
 29         print('test_add')
 30         self.assertEqual(num, 3, msg='加法错误')
 31 
 32     def test_2_sub(self):
 33         num = 1 - 1
 34         print('test_sub')
 35         self.assertEqual(num, 3, msg='减法错误')
 36 
 37 
 38 if __name__ == '__main__':
 39     # 1. 把测试用例添加到suite容器中
 40     suite = unittest.defaultTestLoader.discover('./', 'test_1_html.py')
 41 
 42     # 2. 打开文件,是一个文件对象
 43     with open('./HTMLTestRunner.html', 'w', encoding='utf-8') as f:
 44         # 3. HTMLTestRunner()创建一个runner对象
 45         runner = HTMLTestRunner(
 46             stream=f,  # 测试报告需要写入到的文件
 47             verbosity=2,  # 控制台输出信息的详细程度, 默认为1
 48             title='这是报告标题',  # 测试报告的标题
 49             description='这是一个测试报告'  # 测试报告的描述
 50         )
 51         # 4. runner把容器中测试用例运行
 52         runner.run(suite)
View Code
  • 通过终端运行,生成测试报告

测试报告1

2、BeautifulReport

  • 安装BeautifulReport插件
  1 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple beautifulreport
  • 实例代码:
  1 import unittest
  2 from BeautifulReport import BeautifulReport
  3 
  4 
  5 class MyTest(unittest.TestCase):
  6     def setUp(self) -> None:
  7         print('setUp')
  8 
  9     def tearDown(self) -> None:
 10         print("tearDown")
 11 
 12     @classmethod
 13     def setUpClass(cls) -> None:
 14         print('setUpClass')
 15 
 16     @classmethod
 17     def tearDownClass(cls) -> None:
 18         print('tearDownClass')
 19 
 20     def test_1_add(self):
 21         num = 1 + 2
 22         print('test_add')
 23         self.assertEqual(num, 3, msg='加法错误')
 24 
 25     def test_2_sub(self):
 26         num = 1 - 1
 27         print('test_sub')
 28         self.assertEqual(num, 3, msg='减法错误')
 29 
 30 
 31 if __name__ == '__main__':
 32     # 1. 把测试用例添加到suite容器中
 33     suite = unittest.defaultTestLoader.discover('./', 'test_2_beautiful.py')
 34 
 35     # 2. 创建runner对象,同时把suite传参进入
 36     runner = BeautifulReport(suite)
 37 
 38     # 3. 运行,同时生成测试报告
 39     # 参数1:生成文件的注释, 参数2:生成文件的filename, 参数3:生成report的文件存储路径
 40     runner.report('报告描述必须有,在报告中显示为用例名称', '测试报告文件名', './')
View Code
  • 生成测试报告

测试报告2

七、综合案例

1、读取json文件数据

# 打开json文件
with open("xxx.json", "r") as f:
    # 读取json文件数据
    json_data = json.load(f)

2、测试目录结果

unittest案例目录结构

posted on 2020-10-19 10:53  yycnblog  阅读(160)  评论(1)    收藏  举报

导航