1. unittest介绍

unittest.png

python自带的一个单元测试框架。

具备完整的测试结构,支持自动化测试的执行,对测试用例进行组织,并提供了丰富的断言方法,最后会生成测试报告。

unittest核心工作原理

unittest中最核心的四个概念是:test case, test suite, test runner, test fixture

  • TestCase:一个testcase的实例就是一个测试用例
  • TestSUite:多个测试用例集合在一起
  • TestLoader:是用来加载Testcase到Testsuite中的
  • TextTestRunner:用来执行测试用例的,其中run(test)会执行testsuite/testcase中的run(result)方法
  • TextTestResult:保存TextTestRunner执行的测试结果
  • fixture:测试用例环境的搭建和销毁

写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,我们通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者我们可以直接通过TextTestRunner来执行用例。这里加个说明,在Runner执行时,默认将执行结果输出到控制台,我们可以设置其输出到文件,在文件中查看结果(你可能听说过HTMLTestRunner,是的,通过它可以将结果输出到HTML中,生成漂亮的报告,它跟TextTestRunner是一样的,从名字就能看出来,这个我们后面再说)。

unittest初级使用

1.导入unittest模块,被测文件或者其中的类
2.创建一个测试类,并继承unittest.Testcase 自己创建的单元测试类都要继承它,是所有单元测试类的基类
3.重写set up和tear down方法(如果有初始化条件和结束条件)
4.定义测试函数,函数名以test_开头,否则无法识别并执行。
5.在函数体中使用断言来判断测试结果是否符合预期结果
6.调用unittest.main()方法运行测试用例,执行顺序是按照case的命名,会搜索当前module下所有以test开头的测试用例并运行

2. 核心概念

2.1 测试脚手架

前置 测试 后置

test fixture:环境的准备以及关联的清理动作。setUp()------前置条件、tearDown()-----后置条件,用于初始化测试用例及清理和释放资源。(setUpClass()/tearDownClass())

  1. setUp():每个测试方法运行前运行,测试前置的初始化工作。
  2. tearDown():每个测试方法结束后运行,测试后的清理工作。
  3. setUpClass():所有的测试方法运行前运行,单元测试类运行前的准备工作。必须使用@classmethod装饰器进行装饰。整个测试类运行过程中只会执行一次。
  4. tearDownClass():所有的测试方法结束后运行,单元测试类运行后的清理工作。必须使用@classmethod装饰器进行装饰。整个测试类运行过程中只会执行一次。

2.2 测试用例

TestCase:是最小的测试单元,用于检测特定的输入集合的特定的返回值。unittest提供了TestCase基类,我们创建的测试类需要继承该基类,它可以用来创建新的测试用例。

测试用例,通过继承unittest.TestCase,来实现用例的继承,Unitest中,测试用例都是通过test来识别的。

def test_xxxx(self):

def xxxx_test(self):

2.3 测试套件

  1. 直接通过unittest.main()方法加载单元测试的测试模块,这是一种最简单的加载方法。所有的测试用例的执行顺序都是按照方法名的字符串表示的ASCII码升序排序。
  2. 将所有的单元测试用例TestCase加载到测试套件Test Suite集合中,然后一次性加载所有测试对象。

test suit是一系列的测试用例

测试套件是测试用例,测试套件或两者的集合。用于组装一组要运行的测试。

测试套件,也称为测试用例集,它用于归档需要一起执行的测试。

Test Suite

if __name__ == '__main__':
    # 创建测试套件
    suit = unittest.TestSuite()
    suit.addTest(TestCalculator("test_add"))
    suit.addTest(TestCalculator("test_sub"))
    suit.addTest(TestCalculator("test_mul"))
    suit.addTest(TestCalculator("test_div"))
 
    # 创建测试运行器
    runner = unittest.TextTestRunner()
    runner.run(suit)
  """
测试用例的执行抛弃了unittest提供的main()方法,而是调用TestSuite类下面的addTest来添加参数用例,因为一次只能添加一条用例,所以需要指定测试类与测试方法。然后,再调用TextTestRunner类下面的run()运行测套件。
  """

Test Loader

该类根据各种标准加载测试用例,并将它们返回给测试套件。正常情况下,不需要创建这个类的实例。unittest提供了可以共享的defaultTestLoader类,可以使用其子类和方法创建实例,discover()就是其中之一。

import unittest
 
test_dir = './test_case'
discover = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py')
 
if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    runner.run(discover)

discover(start_dir,pattern='test*.py',top_level_dir=None)

找到指定目录下所有测试模块,并可递归查到子目录下的测试模块,只有匹配到文件名才能被加载。如果启动的不是顶层目录,那么顶层目录必须单独指定。

  • start_dir:要测试的模块名或测试用例目录
  • pattern='test*.py':表示用例文件名的匹配原则。此处匹配文件名以“test”开头的“.py”类型的文件,幸好“*”表示任意多个字符
  • top_level_dir=None:测试模块的顶层目录,如果没有顶层目录,默认为None

2.4 测试运行器

test runner 一个用于执行和输出测试结果的组件。

TextTestRunner()

# 创建测试运行器
if __name__ == '__main__':
    ts = unittest.defaultTestLoader.discover('./') # 表示收集当前目录下所有用例
    runner = unittest.TextTestRunner()
    runner.run(ts)

beautifulreport

import unittest
from BeautifulReport import BeautifulReport
# beautifulreport
if __name__ == '__main__':
    
    ts = unittest.defaultTestLoader.discover('./') # 表示收集当前目录下所有用例
    runner = BeautifulReport(ts)
    runner.report("py45期第一份测试报告", 'bfreport.html')

unittestreport

import unittest
import unittestreport


if __name__ == '__main__':
    discover = unittest.defaultTestLoader.discover('./')  # 表示收集当前目录下所有用例
    runner = unittestreport.TestRunner(discover, title='py45期第一份测试报告', desc='木森老师的测试报告模板', tester='shisuiyi')
    runner.run()

3. 单元测试案例

3.1 测试目标函数

testcode.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/8 21:08
# @Author  : shisuiyi
# @File    : testcode.py
# @Software: win10 Tensorflow1.13.1 python3.9
def login(username, password):
    """
    登录校验的函数
    :param username: 账号
    :param password: 密码
    :return:
    """
    if 6 <= len(password) <= 18:
        if username == 'python45' and password == 'lemonban':
            return {"code": 0, "msg": "登录成功"}
        else:
            return {"code": 1, "msg": "账号或密码不正确"}
    else:
        return {"code": 1, "msg": "密码长度在6-18位之间"}

3.2 设计用例

根据函数的参数和逻辑,设计如下用例:

序号标题测试数据预期结果实际结果
1账号密码正确{"username":"python45", "password":"lemonban"}{"code": 0, "msg": "登录成功"}
2账号正确密码不正确{"username":"python45", "password":"lemonban123"}{"code": 1, "msg": "账号或密码不正确"}
3账号错误密码正确{"username":"python45123", "password":"lemonban"}{"code": 1, "msg": "账号或密码不正确"}
4账号正确,密码长度小于6{"username":"python45", "password":"lemo"}{"code": 1, "msg": "密码长度在6-18位之间"}
5账号正确,密码长度大于18位{"username":"python45", "password":"lemonbanlemonbanlemonbanlemonbanlemonban"}{"code": 1, "msg": "密码长度在6-18位之间"}

3.3.编写测试用例并运行

编写步骤:

  1. 导入unittest模块,被检测函数或者类

  2. 创建测试类,继承unittest.TestCase

  3. 如果有初始化条件和结束条件需要编写脚手架代码

  4. 定义测试函数,函数名一定要以test开头

  5. 调用unittest.main()方法运行测试用例

    test_login.py

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # @Time    : 2021/11/8 21:08
    # @Author  : shisuiyi
    # @File    : test_login.py
    # @Software: win10 Tensorflow1.13.1 python3.9
    import unittest
    from testcode import login
    
    
    class TestLogin(unittest.TestCase):
        """
        测试登录函数
        """
    
        @classmethod
        def setUpClass(cls) -> None:
            """
            类级前置
            :return:
            """
            print('我是类级前置,在整个类的测试前执行')
    
        @classmethod
        def tearDownClass(cls) -> None:
            """
            类级后置
            :return:
            """
            print('我是类级后置,在整个测试类测试完成后执行')
    
        def setUp(self) -> None:
            """
            方法级前置
            :return:
            """
            print('======我会在每个测试执行前执行====')
    
        def tearDown(self) -> None:
            """
            方法级后置
            :return:
            """
            print('======我会在每个测试执行后执行======')
    
        def test_login_01ok(self):
            """
                测试登录成功
                :return:
                """
            print('1测试登录成功开始测试')
            # 1. 准备测试数据
            test_data = {"username": "python45", "password": "lemonban"}
            expect_data = {"code": 0, "msg": "登录成功"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
        def test_login_02password_error(self):
            """
                测试账号正确,密码错误
                :return:
                """
            print('2测试账号正确,密码错误')
            # 1. 准备测试数据
            test_data = {"username": "python45", "password": "lemonban123"}
            expect_data = {"code": 0, "msg": "登录成功"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
        def test_login_03username_error(self):
            """
                测试账号错误,密码正确
                :return:
                """
            print('3测试账号错误,密码正确')
            # 1. 准备测试数据
            test_data = {"username": "python45123", "password": "lemonban"}
            expect_data = {"code": 1, "msg": "账号或密码不正确"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
        def test_login_04short_password(self):
            """
                测试账号正确,密码长度过短
                :return:
                """
            print('4测试账号正确,密码长度过短')
            # 1. 准备测试数据
            test_data = {"username": "python45", "password": "lemo"}
            expect_data = {"code": 1, "msg": "密码长度在6-18位之间"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
        def test_login_05long_password(self):
            """
                测试账号正确,密码长得过长
                :return:
                """
            print('5 测试账号正确,密码长得过长')
            # 1. 准备测试数据
            test_data = {"username": "python45", "password": "lemonbanlemonbanlemonbanlemonbanlemonban"}
            expect_data = {"code": 1, "msg": "密码长度在6-18位之间"}
            # 2. 测试步骤
            res = login(**test_data)
            # 3. 断言
            self.assertEqual(res, expect_data)
    
    
    if __name__ == '__main__':
        unittest.main()
    
    
    # 输出:
    C:\Users\12446\AppData\Local\Programs\Python\Python39\python.exe C:\Users\12446\AppData\Local\JetBrains\Toolbox\apps\PyCharm-C\ch-0\212.5457.59\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py --path D:/Lemon/py45/day16/test_login.py
    Testing started at 21:16 ...
    Launching unittests with arguments python -m unittest D:/Lemon/py45/day16/test_login.py in D:\Lemon\py45\day16
    
    我是类级前置,在整个类的测试前执行
    ======我会在每个测试执行前执行====
    1测试登录成功开始测试
    ======我会在每个测试执行后执行======
    ======我会在每个测试执行前执行====
    2测试账号正确,密码错误
    ======我会在每个测试执行后执行======
    ======我会在每个测试执行前执行====
    3测试账号错误,密码正确
    ======我会在每个测试执行后执行======
    ======我会在每个测试执行前执行====
    4测试账号正确,密码长度过短
    ======我会在每个测试执行后执行======
    ======我会在每个测试执行前执行====
    5 测试账号正确,密码长得过长
    ======我会在每个测试执行后执行======
    我是类级后置,在整个测试类测试完成后执行
    
    
    Ran 5 tests in 0.022s
    
    OK
    
    Process finished with exit code 0
    
    

3.4 测试脚手架

unittest的脚手架有4个?

  • setUp 在每个单元测试开始之前执行
  • tearDown在每个单元测试结束之后执行
  • setUpClass 在整个测试用例类开始前执行
  • tearDownClass 在整个测试用例类测试结束之后执行

3.5 单元测试的执行顺序

按照单元测试函数名的ASCII编码来排序。

3.6 断言方法

方法检查
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
assertIn(a,b)a in b
assertNotIn(a,b)a not in b

4. 测试套件

测试套件就是收集所有的测试用例,然后集中执行。

4.1 通过TestSuite对象收集

这个麻烦,实际不用

4.2 通过TestLoader对象收集

unittest.defaultTestLoader.discover()

discover方法可以根据给定的目录,去自动查找目录下所有符合要求的测试用例,它接受三个参数:

  • start_dir 需要查找的目录
  • pattern 查找模块名的规则,保持默认就好
  • top_level_dir 项目的顶层目录,保持默认就好

注意了:pattern=test*.py参数的规则,需要收的模块名必须以test开头

5. 执行用例&生成报告

直接输出的报告比较丑陋,比如TextTestRunner(unittest自带的)

  • HTMLTestRunnerNew 未知作者,更新什么的不靠谱,不用了

  • BeautifulReport 通过pip可以安装

    • pip install beautifulreport
  • unittestreport 零檬班-木森老师开发的

    • pip install unittestreport

main.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2021/11/9 20:14
# @Author  : shisuiyi
# @File    : main.py
# @Software: win10 Tensorflow1.13.1 python3.9
import unittest
"""
项目的入口文件
"""
import unittest

import unittestreport

from BeautifulReport import BeautifulReport

from test_login import TestLogin

if __name__ == '__main__':
    # # 1. 实例化一个测试套件对象
    # ts = unittest.TestSuite()
    # # 一个一个的添加单元测试
    # # 语法是测试用例类('单元测试方法')
    # ts.addTest(TestLogin('test_login_01ok'))
    # ts.addTest(TestLogin('test_login_02password_error'))
    # # 多个添加
    # ts.addTests([TestLogin('test_login_03username_error'),
    #              TestLogin('test_login_04short_password'),])
    # 运行
    # testloader
    ts = unittest.defaultTestLoader.discover('./') # 表示收集当前目录下所有用例
    # runner = unittest.TextTestRunner()
    # # 传入套件
    # runner.run(ts)

    # beautifulreport
    # br = BeautifulReport(ts)
    # br.report("py45期第一份测试报告", 'bfreport.html')

    # unittestreport
  runner = unittestreport.TestRunner(discover, title='py45期第一份测试报告', desc='老师的测试报告模板', tester='shisuiyi')
    runner.run()

6.忽略某个测试方法

unittest提供了一些跳过指定用例的方法

  • @unittest.skip(reason):强制跳转。reason是跳转原因
  • @unittest.skipIf(condition, reason):condition为True的时候跳转
  • @unittest.skipUnless(condition, reason):condition为False的时候跳转
  • @unittest.expectedFailure:如果test失败了,这个test不计入失败的case数目
import sys
import unittest


class Test001(unittest.TestCase):
    @unittest.expectedFailure  # 即使报错了,也会被计为成功的用例
    def test_1(self):
        print("1+1...", 2)
        assert 1 + 1 == 3

    @unittest.skip('无条件的忽略')  # 不管什么情况都会进行忽略
    def test_2(self):
        print("2+2...", 4)

    @unittest.skipIf(sys.platform == "win32", "跳过")  # 如果系统平台为window则忽略
    def test_3(self):
        print("3+3...", 6)

    @unittest.skipUnless(sys.platform == "win32", "跳过")  # 跳过该用例,除非系统平台为window
    def test_4(self):
        print("4+4...", 8)

    def test_5(self):
        print("5+5...", 10)


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

执行结果:

test_1 (__main__.Test001) ... 1+1... 2
expected failure
test_2 (__main__.Test001) ... skipped '无条件的忽略'
test_3 (__main__.Test001) ... skipped '跳过'
test_4 (__main__.Test001) ... 4+4... 8
ok
test_5 (__main__.Test001) ... 5+5... 10
ok

----------------------------------------------------------------------
Ran 5 tests in 0.005s

OK (skipped=2, expected failures=1)

可以看到skip有两个分别是test_2和test_3,因为他们的忽略条件都成立了。

7.HTML 测试报告生成

unittestteport 中封装了一个 TestRunner 类,可以用来代替 unittest 中的 TextTestRunner 来执行测试用例,执行完测试用例之后会自动生成测试报告。并且有多种报告风格可选

# 模块导入
from unittestreport import TestRunner
# 使用案例
runner = TestRunner(test_suite)
runner.run()
# 关于 TestRunner 初始化参数
# suites: 测试套件(必传)
# filename: 指定报告文件名
# report_dir:指定存放报告路径
# title:指定测试报告的标题
# templates: 可以指定 1,2,3 三个风格的模板
# tester:测试人员名称

8、测试用例失败重运行

关于unittest重运行机制,unittestreport中提供了两种方式

方式一: rerun 装饰器

使用案例:使用 rerun 装饰失败需要重运行的用例,该用例失败后会自动重运行

from unittestreport import rerun

class TestClass(unittest.TestCase):
    @rerun(count=4, interval=2)
    def test_case_01(self):
        a = 100
        b = 99
        assert a == b
用例运行
runner = TestRunner(test_suite)
runner.run()

参数说明1:

count:用来指定用例失败重运行的次数
interval:指定每次重运行的时间间隔

方式二:TestRunner.rerun 方法

使用案例:所有的用例失败,只要有失败的用例,会自动重运行该用例 用例正常编写即可

运行是使用 TestRunner.rerun_run 方法运行

runner = TestRunner(suite=suite)
runner.rerun_run(count=3, interval=2)

参数说明2:

count:用来指定用例失败重运行的次数
interval:指定每次重运行的时间间隔

9.邮件发送测试报告

unittestreport 内部实现了发生测试结果到邮箱的方法,执行完测试用例之后调用发送测试报告的方法即可。发邮件的方法介绍:TestRunner 类中实现了 send_email 方法,可以方便用户,快速发送邮件。

使用案例

runner = TestRunner(suite)
runner.run()
runner.send_email(host="smtp.qq.com",
                  port=465,
                  user="xxxxb@qq.com",
                  password="xxxxxxx",
                  to_addrs="xxxxxx@qq.com")

参数介绍 host: smtp 服务器地址 port:端口 user:邮箱账号 password:smtp 服务授权码 to_addrs:收件人邮箱地址(一个收件人传字符串,多个收件人传列表)

10. 发送测试结果到钉钉

关于把如果测试结果发送到钉钉群,unittestreport里面进行了封装。执行完用例之后,调用TestRunner对象的dingtalk_notice方法即可。 参数介绍 关于 dingtalk_notice 这个方法的参数如下,大家可以根据使用需求来进行选择。

  • url: 钉钉机器人的 Webhook 地址
  • key: (非必传:str 类型)如果钉钉机器人安全设置了关键字,则需要传入对应的关键字
  • secret:(非必传:str 类型)如果钉钉机器人安全设置了签名,则需要传入对应的密钥
  • atMobiles: (非必传,list 类型)发送通知钉钉中要@人的手机号列表,如:[137xxx,188xxx]
  • isatall: 是否@所有人,默认为 False,设为 True 则会@所有人
  • except_info:是否发送未通过用例的详细信息,默认为 False,设为 True 则会发送失败用例的详细信息

案例代码:

import unittest
from unittestreport import TestRunner

#### 收集用例到套件
suite = unittest.defaultTestLoader.discover(CASE_DIR)
runner = TestRunner(suite)
# 执行用例
runner.run()
#钉钉开放平台-第三方应用-群机器人-自定义机器人
url = "https://oapi.dingtalk.com/robot/send?access_token=6e2a63c2b9d870ee878335b5ce6d5d10bb1218b8e64a4e2b55f96a6d116aaf50"
# 发送钉钉通知  
runner.dingtalk_notice(url=url, key='钉钉安全设置的关键字',secret='钉钉安全设置签名的秘钥')

备注:关于钉钉群机器人的创建大家可以去看钉钉开放平台上的教程,关键字和秘钥,根据创建钉钉机器人时设置的去添加,没有设置就不需要传这个参数。

11. 多线程运行测试用例

runner.run(thread_count=3)

注意点:

  • 确保每个线程在执行的时候不会出现资源竞争(对全局依赖的数据进行修改)
  • 用例类执行没有先后顺序的依赖关系