1. unittest介绍
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()
)
- setUp():每个测试方法运行前运行,测试前置的初始化工作。
- tearDown():每个测试方法结束后运行,测试后的清理工作。
- setUpClass():所有的测试方法运行前运行,单元测试类运行前的准备工作。必须使用@classmethod装饰器进行装饰。整个测试类运行过程中只会执行一次。
- tearDownClass():所有的测试方法结束后运行,单元测试类运行后的清理工作。必须使用@classmethod装饰器进行装饰。整个测试类运行过程中只会执行一次。
2.2 测试用例
TestCase
:是最小的测试单元,用于检测特定的输入集合的特定的返回值。unittest
提供了TestCase
基类,我们创建的测试类需要继承该基类,它可以用来创建新的测试用例。
测试用例,通过继承unittest.TestCase
,来实现用例的继承,Unitest
中,测试用例都是通过test来识别的。
def test_xxxx(self):
def xxxx_test(self):
2.3 测试套件
- 直接通过unittest.main()方法加载单元测试的测试模块,这是一种最简单的加载方法。所有的测试用例的执行顺序都是按照方法名的字符串表示的
ASCII
码升序排序。 - 将所有的单元测试用例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')
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.编写测试用例并运行
编写步骤:
-
导入
unittest
模块,被检测函数或者类 -
创建测试类,继承
unittest.TestCase
-
如果有初始化条件和结束条件需要编写脚手架代码
-
定义测试函数,函数名一定要以test开头
-
调用
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)
注意点:
- 确保每个线程在执行的时候不会出现资源竞争(对全局依赖的数据进行修改)
- 用例类执行没有先后顺序的依赖关系
评论