1. 介绍
python自带的一个单元测试框架。
具备完整的测试结构,支持自动化测试的执行,对测试用例进行组织,并提供了丰富的断言方法,最后会生成测试报告。
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 测试目标函数
#!/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()
方法运行测试用例#!/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
#!/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.忽略某个测试方法
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,因为他们的忽略条件都成立了。