python自带的unittest框架
1. unittest 框架介绍
框架
1) framework(框架)
2) 概念: 为了解决一类事情的功能集合
Unittest框架
和pytest不同, 是Python 自带的单元测试框架
1) python自带, 不需要单独安装
2) 测试人员用来做自动化测试, 作为自动化测试的执行框架, 用来管理和执行用例的
使用的原因
1) 能够组织多个用例去执行
2) 提供丰富的断言方法
3) 能够生成测试报告
核心组成
1) TestCase 测试用例, 这个测试用例是 unittest的一部分, 是用来写测试用例代码(脚本)
2) Testsuite 测试套件, 用来组装打包TestCase(测试用例), 把多个测试用例脚本文件组装到一起
3) TestRunner 测试执行, 作用: 用例执行 TestSuite(测试套件), 管理某个.py文件的某个类里面的用例
4) TestLoader 测试加载, 是对 TestSuite(测试套件) 功能的补充, 用来组装打包测试用例(更加方便的组装测试用例,把管理xx*.py文件的用例)
5) Fixture 测试夹具, 是一种代码结构, 在执行用例之前, 进行前置操作(登录前要先获取token), 执行完用例后, 进行后置操作(对文件进行操作后要对文件进行关闭操作) 用例执行顺序: 前置 -> 用例 -> 后置
2. TestCase 测试用例
概念
TestCase 这个类是用来写我们的测试用例的脚本(代码)的, 单独的一个测试用例也是可以执行的(类和方法旁边都有一个执行符号, 类的执行符号是执行所有的方法, 方法的执行符号是只执行这一个方法)
步骤
1) 导包 unittest
2) 定义测试类, 需要继承 unittest.TestCase 类, 习惯性类名以 Test 开头
3) 书写测试方法, 必须以 test 开头
4) 执行
注意事项:
1) 代码文件名字 要满足标识符的规则
2) 代码文件名 不要使用中文
代码:
"""
学习 TestCase(测试⽤例) 的使⽤
"""
# 1. 导包 unittest
import unittest# 2. 定义测试类, 只要继承 unittest.TestCase 类, 就是测试类
class TestDemo(unittest.TestCase):# 3. 书写测试⽅法, ⽅法中的代码就是真正⽤例代码,⽅法名必须以 test 开头def test_method1(self):print('测试⽅法⼀')def test_method2(self):print('测试⽅法⼆')# 4. 执⾏
# 4.1 在类名或者⽅法名后边右键运⾏
# 4.1.1 在类名后边, 执⾏类中的所有的测试⽅法
# 4.1.2 在⽅法名后边, 只执⾏当前的测试⽅法
# 4.1 在主程序使⽤使⽤ unittest.main() 来执⾏,
if __name__ == '__main__':unittest.main()
可能出现的错误
1) 文件包含中文
2) 右键运行没有 unittest for ....
解决方案一:
新建⼀个代码⽂件, 将之前的代码复制过来
解决方案二:
在主程序使⽤使⽤ unittest.main() 来执⾏
解决方案三:
点击设置Configuration 移除Python中的内容
3. TestSuite 和 TestRunner
TestSuite(测试套件)
将多条用例脚本集合在一起, 就是套件, 用来组装用例(把多个用例放在一起,方便管理,执行)
步骤:
1) 导包 unittest
2) 实例化套件对象 unittest.TestSuite()
3) 添加用例方法
TestRunner(测试执行)
用来执行套件对象
步骤:
1) 导包 unittest
2) 实例化 执行对象: unittest.TextTestRunner()
3) 执行对象 执行 套件对象: 执行对象.run(套件对象)
整体步骤
1) 导包 unittest
2) 实例化套件对象: unittest.TestSuite()
3) 添加用例方法: 套件对象.addTest(测试类名('测试方法名'))
4) 实例化 执行对象 unittest.TextTestRunner()
5) 执行对象 执行 套件对象: 执行对象.run(套件对象)
代码案例
套件可以用来组装用例, 用例可以来自不同xx.py的类里面的用例
用例代码文件:
TestSuite1.py
# 1. 导包 unittest
import unittest# 2. 定义测试类, 只要继承 unittest.TestCase 类, 就是测试类
class TestDemo1(unittest.TestCase):# 3. 书写测试⽅法, ⽅法中的代码就是真正⽤例代码,⽅法名必须以 test 开头def test_method1(self):print('测试⽅法1-1')def test_method2(self):print('测试⽅法1-2')
套件和执行
TestRunner.py
# 1. 导包 unittest
import unittestfrom TestSuite1 import TestDemo1
from TestSuite2 import TestDemo2# 2. 实例化套件对象 unittest.TestSuite()
suite = unittest.TestSuite()
# 3. 添加⽤例⽅法
# 3.1 套件对象.addTest(测试类名('测试⽅法名')) # 建议复制
suite.addTest(TestDemo1('test_method1'))
suite.addTest(TestDemo1('test_method2'))
suite.addTest(TestDemo2('test_method1'))
suite.addTest(TestDemo2('test_method2'))
# 4. 实例化 执⾏对象 unittest.TextTestRunner()
runner = unittest.TextTestRunner()
# 5. 执⾏对象执⾏ 套件对象 执⾏对象.run(套件对象)
runner.run(suite)
补充: makeSuite()
我们看上面的代码, 会觉得把用例方法一个一个加进套件会很麻烦, 因此我们使用makeSuite, 可以一口气把类里面所有的方法都加入到套件去.
语法: 套件对象.addTest(unittest.makeSuite(含有测试方法的类名))
代码:
# 1) 导包 unittest
import unittestfrom TestSuite1 import TestDemo1
from TestSuite2 import TestDemo2# 2) 实例化套件对象: unittest.TestSuite()
suite = unittest.TestSuite()
# 3) 添加用例方法: 套件对象.addTest(测试类名('测试方法名'))
# 套件对象.addTest(unittest.makeSuite(类名)套件对象.addTest(unittest.makeSuite(类名)) )
suite.addTest(unittest.makeSuite(TestDemo1))
suite.addTest(unittest.makeSuite(TestDemo2))
# 4) 实例化 执行对象 unittest.TextTestRunner()
runner = unittest.TextTestRunner()
# 5) 执行对象 执行 套件对象: 执行对象.run(套件对象)
runner.run(suite)
查看执行结果
.代表用例执行通过, F 代表用例执行不通过, E 表示用例代码错误
4. TestLoader 测试加载
1) 背景: TestLoader 是 TestSuite 的补充, 我们再makeSuite()只能控制一个.py文件的用例, 那如果我们执行的.py很多, 我们就需要 makeSuite()很多次, 因此我们可以使用 TestLoader 来把类再进行一次打包,一口气加入到测试套件里面去
2) TestLoader 也是用来组装用例代码的, 因此同样需要使用 TextTestRunner() 去执行
3) 语法: unittest.TestLoader().discover('⽤例所在的⽬录', '⽤例代码⽂件名*.py')
步骤:
1) 导包 unittest
2) 实例化加载对象并加载用例 --> 得到的是套件对象
3) 实例化执行对象并执行
示例代码:
import unittest# 实例化加载对象并加载⽤例,得到套件对象
# suite = unittest.TestLoader().discover('⽤例所在的⽬录', '⽤例代码⽂件名*.py')
suite = unittest.TestLoader().discover('.', 'TestSuite*.py')
# 实例化执⾏对象并执⾏
unittest.TextTestRunner().run(suite)
练习:
需求1:
1. 创建⼀个⽬录 case, 作⽤就是⽤来存放⽤例脚本,
2. 在这个⽬录中创建 5 个⽤例代码⽂件 , test_case1.py
3. 使⽤ TestLoader 去执⾏⽤例
将来的代码 ⽤例都是单独的⽬录 中存放的
test_项⽬_模块_功能.py
代码
测试用例脚本
test_case1.py
import unittestclass TestDome1(unittest.TestCase):def test_1(self):print("测试用例1-1")def test_2(self):print("测试用例1-2")
套件的执行
test_loader.py
import unittest# 创建套件对象, 并且把测试用例加入进套件
suite = unittest.TestLoader().discover('.', 'test_case*.py')
# 执行套件
unittest.TextTestRunner().run(suite)
需求2:
1. 定义⼀个 tools 模块, 在这个模块中 定义 add 的⽅法,可以对两个数字求和,返回求和结果
2. 书写⽤例, 对 add() 函数进⾏测试
1, 1, 2
1, 2, 3
3, 4, 7
4, 5, 9
-----
之前的测试⽅法,直接⼀个 print
这个案例中的 测试⽅法,调⽤ add 函数, 使⽤ if 判断,来判断
预期结果和实际结果是否相符
预期结果 2 3 7 9
实际结果 调⽤ add()
测试用例: test_add
import unittestfrom tool import addclass TestAdd(unittest.TestCase):def test_1(self):if 3 == add(1, 2):print("用例通过")else:print("用例失败")def test_2(self):if 5 == add(3, 2):print("用例通过")else:print("用例失败")def test_3(self):if 4 == add(1, 2):print("用例通过")else:print("用例失败")
测试套件的执行: suite.py
方法一
import unittest
# 创建测试套件, 并且把测试用例全部导入
suite = unittest.TestLoader().discover('.', 'test_add*.py')
# 执行测试套件
unittest.TextTestRunner().run(suite)
方法二
import unittestfrom case.test_add import TestAdd# 创建测试套件, 并且把测试用例全部导入
suite = unittest.makeSuite(TestAdd)
unittest.TextTestRunner().run(suite)
5. Fixture
为什么要用Fixture以及概念
Fixture 是在用例执行前后都会执行的一种代码结构: 前置操作 -> 执行用例 -> 后置操作
例子:
tpshop 登录
1. 打开浏览器 (⼀次)
2. 打开⽹⻚,点击登录 (每次)
3. 输⼊⽤户名密码验证码1,点击登录 (每次, 测试⽅法)
4. 关闭⻚⾯ (每次)
2. 打开⽹⻚,点击登录 (每次)
3. 输⼊⽤户名密码验证码2,点击登录 (每次, 测试⽅法)
4. 关闭⻚⾯ (每次)
2. 打开⽹⻚,点击登录 (每次)
3. 输⼊⽤户名密码验证码3,点击登录 (每次, 测试⽅法)
4. 关闭⻚⾯ (每次)
5. 关闭浏览器 (⼀次)
其中, 我们 每次打开浏览器才会执行后面的操作, 执行完后面的操作才会关闭浏览器....
方法级别 Fixture
在每个用例执行前后都会自动调用, 方法名是固定的
语法:
def setUp(self): # 前置
# 每个用例执行之前, 都会自动调用
pass
def tearDown(self): # 后置
# 每个用例执行之后, 都会自动调用
pass
执行顺序:
方法前置 用例1 方法后置
方法前置 用例2 方法后置
类级别 Fixture
在类中所有的测试方法执行前后会自动执行的代码, 只会执行一次(每个类执行一次)
语法:
@classmethod
def setUpClass(cls): # 类前置
pass
@classmethod
def tearDownClass(cls): # 类后置
pass
执行顺序:
类前置 方法前置 用例1 方法后置 方法前置 用例2 方法后置 类后置
模块级别 Fixture
模块代表 .py文件, 模块级别 是在这个代码文件执行的时候执行一次
语法:
# 在类外部定义函数
def setUpModule(): # 模块前置
pass
def tearDownModule(): # 模块后置
pass
执行顺序:
模块前置 类1前置 方法前置 用例1 方法后置 方法前置 用例2 方法后置 类1后置
类2前置 方法前置 用例1 方法后置 方法前置 用例2 方法后置 类2后置 模块后置
代码:
测试用例代码: main.py
import unittest# 模块级别Fixture
def setUpModule():print("我是模块级别的前置....")def tearDownModule():print("我是模块级别的后置....")print("---------------------------")class TestFixture(unittest.TestCase):# 类级别Fixture@classmethoddef setUpClass(cls):print("我是类级别的前置....")@classmethoddef setTearDown(cls):print("我是类级别的后置....")print("---------------------------")# 方法级别Fixturedef setUp(self):print("我是方法级别的前置....")def tearDown(self):print("我是方法级别的后置....")print("---------------------------")# 测试用例def test_01(self):print("测试方法1.1")def test_02(self):print("测试方法1.2")def test_03(self):print("测试方法1.3")class TestFixture2(unittest.TestCase):# 类级别Fixture@classmethoddef setUpClass(cls):print("我是类级别的前置2....")@classmethoddef setTearDown(cls):print("我是类级别的后置2....")print("---------------------------")# 方法级别Fixturedef setUp(self):print("我是方法级别的前置2....")def tearDown(self):print("我是方法级别的后置2....")print("---------------------------")# 测试用例def test_01(self):print("测试方法2.1")def test_02(self):print("测试方法2.2")def test_03(self):print("测试方法2.3")
执行套件: suite.py
import unittestfrom main import TestFixture, TestFixture2# suite = unittest.TestLoader().discover('⽤例所在的⽬录', '⽤例代码⽂件名*.py')
suite = unittest.TestLoader().discover('.', 'main.py')
unittest.TextTestRunner().run(suite)
执行结构:
D:\python语法练习\Scripts\python.exe D:\pythonProject3\suite.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000sOK
我是模块级别的前置....
我是类级别的前置....
我是方法级别的前置....
测试方法1.1
我是方法级别的后置....
---------------------------
我是方法级别的前置....
测试方法1.2
我是方法级别的后置....
---------------------------
我是方法级别的前置....
测试方法1.3
我是方法级别的后置....
---------------------------
我是类级别的前置2....
我是方法级别的前置2....
测试方法2.1
我是方法级别的后置2....
---------------------------
我是方法级别的前置2....
测试方法2.2
我是方法级别的后置2....
---------------------------
我是方法级别的前置2....
测试方法2.3
我是方法级别的后置2....
---------------------------
我是模块级别的后置....
---------------------------Process finished with exit code 0
6. 断言
概念
使用代码自动的判断预期解决和实际结果是否相符
assertEqual(预期结果, 实际结果)
判断预期结果和实际结果是否相等, 如果相等, 用例通过, 如果不相等, 抛出异常, 用例不通过.
assertIn(预期结果, 实际结果)
判断预期结果是否包含在实际结果中, 如果存在, 用例通过, 如果不存在, 抛出异常, 用例不通过
练习
1. 定义一个 tools 模块, 在这个模块中 定义 add 的方法,可以对两个数字求和,返回求和结果
2. 书写用例, 对 add() 函数进行测试
1, 1, 2
1, 2, 3
3, 4, 7
4, 5, 9
测试用例脚本编写:
import unittestfrom tool import addclass TestAssert(unittest.TestCase):def test_1(self):self.assertEqual(2, add(1, 1))def test_2(self):self.assertEqual(5, add(3, 2))def test_3(self):self.assertIn([1, 2], [[1, 2], 1, 4, 5])
测试套件组装与执行
import unittestfrom test_assert import TestAssertsuite = unittest.makeSuite(TestAssert)
unittest.TextTestRunner().run(suite)
7. 参数化
概念+环境准备
概念: 通过参数的方式来传递数据, 从而实现数据和脚本的分离.(在书写用例方法的时候,测试数据使用变量代替,在执行发时候进行数据传递)
unittest 测试框架, 本身不支持参数化, 但是可以通过安装 unittest插件, 使用 parameterized 来实现参数化
环境准备:
使用 pip install parameterized 来下载(在 pycharm 的 terminal 终端进行下载)
如果嫌慢, 也可以使用镜像: pip install -i https://pypi.douban.com/simple/ parameterized
使用
1) 导包 from parameterized import parameterized (如果下载后一个.py里面没有导入, 我们可以重新创建一个,复制进去)
2) 修改测试方法, 将测试方法中的测试数据使用 变量表示
3) 组织测试数据, 格式 [(),(),()] ,一个元组就是一组测试数据
4) 参数化, 在测试方法伤使用装饰器 @parameterized.expand(测试数据)
5) 运行(直接 TestCase 或者 挥着 suite 运行)
代码
import unittestfrom parameterized import parameterizedfrom tool import adddata = [(1, 1, 2), (1, 2, 3), (2, 2, 4)]class TestParameterized(unittest.TestCase):@parameterized.expand(data)def test_1(self, a, b, expect):self.assertEqual(expect, add(a, b))if __name__ == '__main__':unittest.main()
对应图:
练习
将测试数据 定义为 json 文件, 读取 json 文件,完成参数化
json文件
[
[1,2,3],
[2,3,5],
[1,22,23]
]
读取json文件
import json# 读取 json 文件
def build_add_data():with open('data.json') as f:data = json.load(f) # [[],[],[]]->[(),()]return data
代码文件
import unittestfrom parameterized import parameterizedfrom read_json_test import build_add_data
from tool import addclass TestParameterized(unittest.TestCase):# 读取的是json数据@parameterized.expand(build_add_data())def test_1(self, a, b, expect):self.assertEqual(expect, add(a, b))if __name__ == '__main__':unittest.main()
对于读取复杂的json数据, 我们还是以加法那个为例
json
[
{
"a": 1,
"b": 2,
"expect": 3
},
{
"a": 11,
"b": 22,
"expect": 33
},
{
"a": 12,
"b": 23,
"expect": 35
},
{
"a": 14,
"b": 25,
"expect": 39
}
]
读取json文件代码
import json# 读取 json 文件
def build_add_data():with open('data.json') as f:data_list = json.load(f) # [{},{},{}]->[(),(),()]# 为了把[{},{},{}]->[(),(),()],我们进行下面操作# 创建一个新数组new_list = []# 构造元组元素for data in data_list: # 取出来是一个个data字典a = data.get('a')b = data.get('b')expect = data.get('expect')# 加入到新数组里面new_list.append((a, b, expect))return new_listdef build_add_data2():with open('data.json') as f:data_list = json.load(f)new_list = []# 进行这个操作的时候, 要判断是否字典里面的值都需要for data in data_list:new_list.append(tuple(data.values()))return new_list
参数化+执行
import unittestfrom parameterized import parameterizedfrom read_json_test import build_add_data
from tool import addclass TestParameterized(unittest.TestCase):# 读取的是json数据@parameterized.expand(build_add_data())def test_1(self, a, b, expect):self.assertEqual(expect, add(a, b))if __name__ == '__main__':unittest.main()
8.测试报告
概念+环境配置
使用第三方的报告模板, 生成报告 HTMLTestReport, 本质是 TestRunner
安装第三方插件: pip install HTMLTestReport
如果觉得慢,还可以使用镜像: pip install -i https://pypi.douban.com/simple/ HTMLTestReport
使用步骤
1) 导包: unittest, HTMLTestReport
2) 组装用例(套件, loader)
3) 使用 HTMLTestReport 中的 runner 执行套件
4) 查看报告
代码:
承接上面的代码,
import unittest
from htmltestreport import HTMLTestReport
from test_assert import TestAssert
from test_parameterized import TestParameterizedsuite = unittest.makeSuite(TestParameterized)
# runner = HTMLTestReport(报告的文件路径后缀.html, 报告的标题, 其他的描述信息)
runner = HTMLTestReport('./report/test_report', "测试报告", 'xxxx')
runner.run(suite)
使用绝对路径
使用原因和方法
原因
将来的项目是分目录书写的,如果使用相对路径,如果目录变更(前面层级),可能会出现找不到文件的情况,因此需要使用绝对路径.
方法
1) 在项目的根目录, 创建一个 Python文件(app.py 或者 config.py)
2) 在这个文件中 获取项目的目录. 在其他代码中使用路径拼接完成绝对路径的书写
获取当前文件的绝对路径: abspath = os.path.abspath(__file__)
获取文件路径的目录名称: dirname = os.path.dirname(filepath)
代码:
import os# 获取文件的绝对路径
path1 = os.path.abspath(__file__)
print(path1)
# 获取文件绝对路径(不包含当前文件名)
path2 = os.path.dirname(path1)
print(path2)
# 合成一个来写
BASE_DIR = os.path.dirname(__file__)
print(BASE_DIR)
案例
需求:
1, 对登录函数进行测试, 登录函数 定义在 tools.py 中
2, 在 case 目录中书写用例对login 函数进行测试, 使用断言
3, 将 login 函数的测试数据定义在 json 文件中,完成参数化, data 目录中
4, 生成测试报告 report 目录中
代码:
json数据:
[
{
"desc": "正确的用户名和密码",
"username": "admin",
"password": "123456",
"expect": "登录成功"
},
{
"desc": "错误的用户名",
"username": "admi2n",
"password": "123456",
"expect": "登录失败"
},
{
"desc": "错误的密码",
"username": "admin",
"password": "1234569",
"expect": "登录失败"
},
{
"desc": "错误的用户名和密码",
"username": "admin1",
"password": "1234526",
"expect": "登录失败"
}
]
read_data.py: 读取json文件
import json
from config import BASE_DIR# 读取json文件
def read_login_data():# 绝对路径拼接读取json文件with open(BASE_DIR + "\data\\\\login.json", encoding='utf-8') as f:data_list = json.load(f)new_list = []# 构造符合parameterized 的数据[{},{}..]=>[(),()...]for data in data_list:# 字典中的 desc不需要username = data.get('username')password = data.get('password')expect = data.get('expect')new_list.append((username, password, expect))return new_list
tool.py: 判断是否登录成功的函数
def login(username, password):if username == "admin" and password == "123456":return "登录成功"else:return "登录失败"
test_login.y: 测试登录的用例
import unittest
from parameterized import parameterizedfrom read_data import read_login_data
from tool import loginclass TestLogin(unittest.TestCase):@parameterized.expand(read_login_data)def test_01(self, username, password, expect):print(f"username:{username},password:{password},expect:{expect}")self.assertEqual(expect, login(username, password))
login_suite.py: 测试套件的执行+测试报告的生成
import unittest
from htmltestreport import HTMLTestReport
from case.test_login import TestLogin
from config import BASE_DIR# 创建测试套件
suite = unittest.TestSuite()
# 添加测试用例
suite.addTest(unittest.makeSuite(TestLogin))
# 执行用例,并且生成测试报告
runner = HTMLTestReport(BASE_DIR + "/report/login_report.html")
runner.run(suite)
执行生成的测试报告:
9. 跳过
概念
对于一些未完成或者不满足测试条件的测试函数和测试类, 可以跳过执行(简而言之, 不想执行的测试方法, 可以设置为跳过)
直接将测试函数标记为跳过
@unittest.skip('跳过的原因')
根据条件判断测试函数是否跳过
@unittest.skipIf(判断条件,reason='原因') # 判断条件为True, 跳过执行
练习
import unittestversion = 30class TestSkip(unittest.TestCase):@unittest.skip('没原因,就是玩')def test_1(self):print("测试方法一")@unittest.skipIf(version >= 30, '版本号大于等于30,测试方法不执行')def test_2(self):print("测试方法二")def test_3(self):print("测试方法三")if __name__ == '__main__':unittest.main()
运行结果
10. 控制测试用例的执行顺序
1) 使用装饰器
import unittestdef priority(level):def decorator(func):func._priority = levelreturn funcreturn decoratorclass PriorityTestLoader(unittest.TestLoader):def getTestCaseNames(self, testCaseClass):test_names = super().getTestCaseNames(testCaseClass)# 按优先级排序test_names.sort(key=lambda x: getattr(getattr(testCaseClass, x), '_priority', 999))return test_namesclass TestSkip(unittest.TestCase):@priority(1)def test_1(self):print("测试方法一")@priority(3)def test_2(self):print("测试方法二")@priority(2)def test_3(self):print("测试方法三")if __name__ == '__main__':loader = PriorityTestLoader()suite = loader.loadTestsFromTestCase(loader)runner = unittest.TextTestRunner()runner.run(suite)
2) 使用 pytest
使用@pytest.mark.run(order=x)来设置用例执行顺序
# test_order_pytest.py
import pytestclass TestExecutionOrder:@pytest.mark.run(order=1)def test_first(self):print("第一个执行")@pytest.mark.run(order=3)def test_third(self):print("第三个执行")@pytest.mark.run(order=2)def test_second(self):print("第二个执行")# 运行: pytest test_order_pytest.py -vs