统计代码的覆盖率(单元测试)
对于单元测试。需要设计用例。然后编写单元测试用例。执行用例, 需求: 如何统计代码的语句覆盖,需要统计我们的单元测试用例。到底覆盖了哪些语句?
coverage是用来统计Python代码覆盖率的工具,可以高亮显示代码中哪些语句未被执行,哪些执行了,方便单测。coverage也支持分支覆盖率统计,可以生成HTM报告。
安装coverage
执行命令
coverage run test_xxx.py #执行test_xxx.py文件,会自动生成一个覆盖率统计结果文件.coverage
coverage html -d report # 生成htm报告
报告说明
列名 | 说明 |
---|---|
name | 测试执行过的文件名 |
stmts | 测试过的行数 |
miss | 没有覆盖到的行数 |
cover | 该文件的覆盖率 |
missing | 没有被覆盖的行号 |
一、Pytest安装和介绍
pytest是python的一种单元测试框架,是基于unittest衍生出来的新的测试框架,使用起来相对于unittest来说更简单、效率来说更高,pytest兼容unittest测试用例,但是反过来unittest不兼容pytest,所以说pytest的容错性更好一些!在使用交互逻辑上面pytest比unittest更全一些!
- 简单灵活,容易上手,文档丰富;
- 支持参数化,可以细粒度地控制要测试的测试用例;
- 能够支持简单的单元测试和复杂的功能测试,还可以用来selenium/appnium等自动化测试、接口自动化测试(pytest+requests);
- pytest具有很多第三方插件,并且可以自定义扩展,比较好用的如pytest-html(完美html测试报告生成)pytest-rerunfailures(失败case重复执行)、pytest-xdist(测试用例分布式执行,多CPU分发)pytest-ordering(用于改变测试用例的执行顺序) allure-pytest(用于生成完美的测试报告)pytest-repeat(重复运行)等;
- 测试用例的skip和xfail处理;
- 支持运行由Nose , Unittest编写的测试Case
- 可以很好的和CI工具结合,例如jenkins=CI工具
pip install pytest
pytest官网 http://pytest.org
校验 : pytest —version 查看版本信息
- 测试发现:从文件中寻找测试用例
- 测试执行:按照规律和顺序进行执行产生结果
- 测试判断:通过断言判断预期结果和实际结果的差异
- 测试报告:统计测试进度,耗时,通过率,生成测试报告
二、Pytest基本使用
- 模块名以test_开头或者 _test结尾的py文件
- 测试类以Test开头,并且不能有init方法
- 以test_开头的函数或方法
- 函数和方法的区别 python中函数和方法的区别 - 马玉刚 - 博客园
-
主函数运行
- pytest.main() 运行当前文件所在目录下的所有case
- pytest.main([“文件名/目录地址”]) 运行指定文件/或目录下的所有case
- 通过nodeid指定用例运行(nodeid由模块名,分割符,类名,方法名,函数名构成)
- pytest main([“文件名::类名::方法名“])
- pytest main([“文件名::方法名”])
-
命令行运行 (了解)
-
pytest 运行当前目录下的所有case
pytest -v -s -
pytest 文件名/文件夹
指定模块,pytest test_1.py ,只运行指定模块里面的所有用例。指定某个包(递归以及子包),执行包和子包下的所有用例。
-
通过nodeid指定用例运行(nodeid由模块名,分割符,类名,方法名,函数名构成)
-
-
通过读取配置文件pytest.ini运行
-
pytest.ini这个文件是pytest单元测试框架的pytest.ini
-
放在项目的根目录
-
不管是命令行运行还是主函数运行,都会先读取这个配置文件
-
只能用ansi编码
[pytest]
# 配置pytest命令行运行参数
# 空格分隔,可添加多个命令行参数 -所有参数均为插件包的参数
addopts = -s
# 配置测试搜索的路径
# 当前目录下的testcase文件夹 -可自定义
testpaths = ./testcase
# 配置测试搜索的文件名
# 当前目录下的testcase文件夹下,以test_开头,以.py结尾的所有文件 -可自定义
python_files = test_*.py
# 配置测试搜索的测试类名
# 当前目录下的testcase文件夹下,以test_开头,以.py结尾的所有文件中,以Test_开头的类 -可自定义
python_classes = Test*
# 配置测试搜索的测试函数名
# 当前目录下的testcase文件夹下,以test_开头,以.py结尾的所有文件中,以Test_开头的类内,以test_开头的方法 -可自定义
python_functions = test_*
-
- -s 用来输出调试信息包括print的信息
- -v 用来显示更多详细的信息
- -q 用来减少详细的信息 只保留关键信息
- 通常我们会 -vs 或者-qs 使用 看个人需求
- -n 支持多进程(每个进程启一个线程)分布式运行用例 指定进程数量 -n=2 需要安装 pytest-xdist
- —reruns 失败重跑 —reruns=2 指定次数为两次 需要安装 pytest-rerunfailures 两个—
- -x 只要出现一个失败用例就停止
- —maxfail=3 出现3个用例执行失败就停止
- -k 根据表达式去查找符合的测试用例 匹配的是文件名、类名、函数名。用or来添加多个用例,用and来添加同时满足多个条件的用例,用not 来排除不执行的用例。
- -k “add” 查找包含add 的case用例
- -k “not add” 查找不包含add 的case用例
-
unittest通过ascii码的大小来决定执行的顺序
-
pytest通过从上到下的顺序来决定执行的顺序(默认)
- 可以使用@pytest.mark.run(order=num) 进行标记 需要安装 pip install pytest-ordering
如何分组执行(冒烟用例执行,分模块执行,分接口和web执行)
# 在配置文件中配置分组
# 不设置会产生警告
markers =
sales:销售模块
login:登陆模块
user:用户模块
smoke:冒烟模块
给要设置的用例设置分组
class TestLogin():
@pytest.mark.smoke
def testsales(self): # test开头的测试函数
print("------->test_a")
assert 1 # 断言成功
@pytest.mark.user
def testaddsale(self):
print("------->test_b")
assert 0 # 断言失败
if __name__ == '__main__':
pytest.main(["-s","-m smoke or user"]) # 调用pytest的main函数执行测试
单个模块执行: pytest -m “模块名”
多个模块执行: pytest -m “模块名1 or 模块名2 “
@pytest.mark.skip(reason="原因")
@pytest.mark.skipif(判断式,reason="原因")
—html=报告生成地址 需要安装pytest-html 一般不用(用allure代替)
pytest结合allure-pytest插件生成allure测试报告
- 去官网https://github.com/allure-framework/allure2/releases下载allure的包
- 下载完成后在本地解压,解压完之后直接放到 D盘下
- 然后配置路径
- 验证电脑终端输入 allure—version pycharm提供的终端也输入allure—version 验证
- 注意事项: allure 是java写的程序 电脑上需要java1.8+的环境
- 电脑输入 allure—version有效果 pycharm没效果的解决方案
- 以管理员权限运行pycharm
- 在环境变量配置的时候 用户变量和系统变量里面的path都配置
- pycharm重启
- 以上三种方案轮流试
- 需要安装pip install allure-pytest
- 添加参数 —alluredir=temp 生成json格式的临时报告
- —clean-alluredir 参数可以清空 上次allure 报告生成的目录 通常 —alluredir=temp —clean-alluredir
- 在终端当前项目文件夹 输入命令 allure generate ./temp -o ./report —clean
- 使用allure 生成 报告
- ./temp 表示找temp下的数据
- -o ./report 表示报告输出到当前目录下的report目录
- —clean 表示清空原来的报告
- 也可以 在运行代码后添加os.system(“allure generate ./temp -o ./report —clean”)
对于接口代码,如登陆,常常会有多种情况的登陆,但其实本质就是每次发送登陆的请求参数不一样,我们如何只写一个测试接口,实现我们多条不同数据的登陆呢?
'''
pytest fixture仓库 ,全部是都fixture ,
'''
import pytest
from global_session import global_session as s
@pytest.fixture()
def login():
'''
封装登录 前置步骤
:return:
'''
url = "http://localhost:端口/woniusales/user/login"
x = {
"username": "admin",
"password": "123456",
"verifycode": "1"
}
s.post(url = url , data = x) # 发送post请求登录
print(id(s))
yield
# 退出
s.close()
核心配置文件
[pytest]
addopts = -v -s --setup-show test_cases/test_batch_upload.py --alluredir=temp --clean-alluredir
testpaths = ./test_cases
markers=
smoke: smoking test case
输出结果
test_cases/test_batch_upload.py::test_batch_upload
SETUP S _session_faker
SETUP F login
test_cases/test_batch_upload.py::test_batch_upload (fixtures used: _session_faker, login, request)
PASSED
TEARDOWN F login
TEARDOWN S _session_faker
使用@pytest.fixture()装饰器实现部分用例的前后置
@pytest.fixture(scope=””,params=””,autouse=””,ids=””,name=””)
参数解释:
-
scope表示的是被@pytest.fixture标记的方法的作用域
- function 默认 针对函数
- class 针对类
- module 针对模块
- package 针对包(没有实现)
- session 针对整个测试会话 开始执行pytest到结束 算一个测试会话
-
params 实现参数化
-
autouse=True 自动使用,默认是false,当为True时表示在它所作用域的范围内都会自动使用该装置
-
ids 当使用params参数化时,给每一个值设置一个变量名。意义不大
-
name 给被@pytest.fixture()标记的方法取一个别名,意义不大,起了别名后,原来的函数名称就不能使用
-
关于形参中出现 / * 的意义 python函数参数中的/和*是什么意思? - 知乎
- fixture默认是函数级别的,还有session级别,这2个最常用。module级别的也偶尔见到。
3、调用fixture的四种方法
(1)函数或类里面方法直接传fixture的名称作为用例函数参数名称
import pytest
@pytest.fixture()
def my_fixture():
print("这是我自己定义的前置的方法")
yield # 通过yield关键字 在后面填写后置的方法
print("这是我自己定义的后置的方法")
class TestLogin():
@pytest.mark.smoke
def testsales(self): # test开头的测试函数
print("------->test_a")
assert 1 # 断言成功
@pytest.mark.user
def testaddsale(self,my_fixture):
print("------->test_b")
assert 0 # 断言失败
if __name__ == '__main__':
pytest.main(["-s","test_aaa.py"]) # 调用pytest的main函数执行测试
(2)使用装饰器@pytest.mark.usefixtures(fixture_name)修饰需要运行的用例 。
usefixtures装饰器除了可以装饰函数和类里面的方法外,还可以装饰类,而且其优先级还比传参方式的优先级高
-
验证 @pytest.mark.usefixtures 和传参的形式的优先级
# 验证 @pytest.mark.usefixtures 和传参的形式的优先级
@pytest.fixture(scope="function")
def function1_fixture(request):
print("这是我自己定义的function1前置的方法")
yield # # 通过yield关键字 在后面填写后置的方法
print("这是我自己定义的function1后置的方法")
@pytest.fixture(scope="function")
def function2_fixture(request):
print("这是我自己定义的function2前置的方法")
yield # # 通过yield关键字 在后面填写后置的方法
print("这是我自己定义的function2后置的方法")
class TestLogin():
@pytest.mark.usefixtures('function1_fixture')
def testsales(self,function2_fixture): # test开头的测试函数
print("------->test_a")
assert 1 # 断言成功
def testaddsale(self):
print("------->test_b")
assert 0 # 断言失败
class TestLogin1():
def testsales(self): # test开头的测试函数
print("------->test_a")
assert 1 # 断言成功
def testaddsale(self):
print("------->test_b------------>")
assert 1 # 断言失败
if __name__ == '__main__':
pytest.main(["-vs", "test_aaa.py"]) # 调用pytest的main函数执行测试
(3)验证类 使用fixture 及autouse的使用 及 不同级别作用域的fixture的优先级
import pytest
@pytest.fixture(scope=”module”,autouse=True)
def module_fixture(request):print("这是我自己定义的module前置的方法")
yield # # 通过yield关键字 在后面填写后置的方法
print("这是我自己定义的module后置的方法")
@pytest.fixture(scope=”class”)
def class_fixture(request):print("这是我自己定义的class前置的方法")
yield # # 通过yield关键字 在后面填写后置的方法
print("这是我自己定义的class后置的方法")
@pytest.fixture(scope=”function”)
def function1_fixture(request):print("这是我自己定义的function1前置的方法")
yield # # 通过yield关键字 在后面填写后置的方法
print("这是我自己定义的function1后置的方法")
@pytest.fixture(scope=”function”)
def function2_fixture(request):print("这是我自己定义的function2前置的方法")
yield # # 通过yield关键字 在后面填写后置的方法
print("这是我自己定义的function2后置的方法")
@pytest.mark.usefixtures(‘class_fixture’)
class TestLogin():@pytest.mark.usefixtures('function1_fixture')
def testsales(self,function2_fixture): # test开头的测试函数
print("------->test_a")
assert 1 # 断言成功
def testaddsale(self):
print("------->test_b")
assert 0 # 断言失败
class TestLogin1():
def testsales(self): # test开头的测试函数
print("------->test_a")
assert 1 # 断言成功
def testaddsale(self):
print("------->test_b------------>")
assert 1 # 断言失败
if name == ‘main‘:
pytest.main([“-vs”, “test_aaa.py”]) # 调用pytest的main函数执行测试
- 不
同作用域的fixture,遵循:session > package > module > class > function
- 相同级别的fixture,且有依赖关系的fixture:遵循fixture之间的依赖关系
fixture 可以使用同级别或者以上级别的fixture
- fixture设置autouse=True
- 使用conftest.py文件共享fixture实例(主要用户项目的全局登陆,模块的全局处理,说白了就是其他地方都会调用的fixture可以写在conftest.py中)
-conftest.py单独用来存放固件的一个配置文件,名称不能更改
- 作用:可以在不同的py文件中使用同一个fixture函数。使用的时候也不需要导入conftest.py,pytest会自动寻找
- 一个工程下可以建多个conftest.py的文件,不同子目录下也可以放conftest.py的文件
- conftest.py文件的作用域是当前包内(包括子包)。如果有函数调用fixture,会优先从当前测试类中寻找,然后是模块(.py文件)中,接着是当前包中寻找(conftest.py中),如果没有再找父包直至根目录;如果我们要声明全局的conftest.py文件,我们可以将其放在项目根目录下。
注意事项:
1. 如果一个测试用例,用到了多个fixture(少见),次序是,范围大的先执行,相同级别的。从做到右执行。
2. fixture可以使用相同级别或者 级别高的fixture
3. class级别的,并不是说,只能在class 里面的测试方法可以用!!!包括测试函数也可以用(行为 = 函数级别),只是测试用例封装在类里面。多个类里面的测试用例,class级别的fixture只会执行一次
4、使用usefixture装饰器指定使用fixture
- 相同级别的fixture,同时用装饰器和参数传递方式调用: usefixtures装饰的优先级高于传参形式的fixture
- 相同级别的fixture,都使用装饰器调用时,先执行的放底层,后执行的放上层
4、实现参数化
#(1)使用@pytest.fixture() 实现参数化
import pytest
# 数据fixture 返回测试数据
@pytest.fixture(params=[{"name":"zs"}, [1,2,3], "c"])
def my_fixture(request):
print("这是我自己定义的前置的方法")
yield request.param # # 通过yield关键字 在后面填写后置的方法
print("这是我自己定义的后置的方法")
class TestLogin():
@pytest.mark.user
def testaddsale(self, my_fixture):
print("------->test_b------------>",my_fixture)
assert 1 # 断言失败
if __name__ == '__main__':
pytest.main(["-vs", "test_aaa.py"]) # 调用pytest的main函数执行测试
(2)使用@pytest.mark.parametrize()实现参数化 ,对测试函数进行参数化(直接)
-
里面写两个参数,第一个参数是字符串,如果要表示多个参数每个参数中间用逗号隔开,第二个参数是list,表示传入的参数化数据
import pytest
class TestLogin():
@pytest.mark.parametrize("data",[1,2,3])
def test_1(self,data): # test开头的测试函数
print(f"------->test_1---------->data:{data}")
@pytest.mark.parametrize("name,age",[['zs',18],['ls',15]])
def test_2(self,name,age):
print(f"------->test_2------->name:{name}---------age:{age}")
@pytest.mark.parametrize("data",[['zs',18],['ls',15]])
def test_3(self,data):
print(f"------->test_3------->data:{data},data的type:{type(data)}")
@pytest.mark.parametrize("data",[{"name":"zs","age":18},{"name":"ls","age":20}])
def test_4(self,data):
print(f"------->test_4------->data:{data},data的type:{type(data)}")
if name == ‘main‘:
pytest.main([“test_aaa.py”]) # 调用pytest的main函数执行测试
```
- 装饰器有2个必填参数, 第一个参数,是字符串 ,第二个参数是列表( 元组,列表,多个字典)
@pytest.mark.parametrize()参数详解 11、Pytest之@pytest.mark.parametrize使用详解_测试工程师Jane的博客-CSDN博客_pytest.mark.parametrize
pytest测试框架:
run.py
#在python大型项目中,一定有且仅有一个启动模块,程序从这个模块开始运行,其他模块是希望被导入 import pytest import os if __name__ == '__main__': # pytest.main(["-vs"]) #执行所有用例 # # pytest.main(["-vs", "test_cases/productquery_module/"]) # 执行某一模块下的所有用例 # # pytest.main((["-vs","test_cases/productquery_module/test_product.py::Test_Mall::test_productchaxun"])) #执行某一模块下的某一用例 pytest.main() os.system("allure generate temp -o reports --clean") #生成报告并且覆盖掉之前的报告
pytest.ini
[pytest] addopts=-v -s "./test_cases/login_module/test_login.py" --alluredir=temp --clean-alluredir testpaths=./mall
Dockerfile
# 基于镜像基础 FROM python:3.6 # 复制当前代码文件到容器中 /app ADD . /app # 设置app文件夹是工作目录 /app WORKDIR /app # run 是在构建镜像的执行 RUN pip install requests -i http://pypi.douban.com/simple --trusted-host=pypi.douban.com RUN pip install pytest -i http://pypi.douban.com/simple --trusted-host=pypi.douban.com RUN pip install allure-pytest -i http://pypi.douban.com/simple --trusted-host=pypi.douban.com # Run run .py when the container launches CMD ["python", "/app/run.py"]
conftest.py
import pytest import requests @pytest.fixture() #声明一个fixture 希望它在测试用例执行之前,执行,login不再是普通函数,而是pytest的fixture def sccess_token(): url = "http://服务器ip:端口/admin/login" payload = { "password": "macro123", "username": "admin" } response = requests.post(url=url, json=payload).json() return response["data"]["token"]
test_cases----coupon_module-----test_coupon.py
import requests import json import pytest class Test_Copo: def test_coupon1(self,sccess_token): url = "http://服务器ip:端口/coupon/create" with open(r"D:\Python\pycharm\API-TESTS\mall\test_cases\coupon_module\test_data.json", mode="rt", encoding="utf8") as x: # 打开json文件 result = json.load(x) # 把json文件转化成对应格式字典 payload = json.dumps(result[0][0]) headers = { 'Authorization': f'Bearer {sccess_token}', 'Content-Type': 'application/json' } response = requests.post(url=url, headers=headers, data=payload) #=================断言======================== assert response.status_code==result[0][1]["code"],"用例测试不通过" def test_coupon2(self,sccess_token): url = "http://服务器ip:端口/coupon/create" with open(r"D:\Python\pycharm\API-TESTS\mall\test_cases\coupon_module\test_data.json", mode="rt", encoding="utf8") as x: # 打开json文件 result = json.load(x) # 把json文件转化成对应格式的列表或者字典 payload = json.dumps(result[1][0]) headers = { 'Authorization': f'Bearer {sccess_token}', 'Content-Type': 'application/json' } response = requests.post(url=url, headers=headers, data=payload) # =================断言======================== assert response.status_code == result[1][1]["code"], "用例测试不通过" def test_coupon3(self,sccess_token): url = "http://服务器ip:端口/coupon/create" with open(r"D:\Python\pycharm\API-TESTS\mall\test_cases\coupon_module\test_data.json", mode="rt", encoding="utf8") as x: # 打开json文件 result = json.load(x) # 把json文件转化成对应格式的列表或者字典 payload = json.dumps(result[2][0]) headers = { 'Authorization': f'Bearer {sccess_token}', 'Content-Type': 'application/json' } response = requests.post(url=url, headers=headers, data=payload) # =================断言======================== assert response.status_code == result[2][1]["code"], "用例测试不通过" def test_coupon4(self,sccess_token): url = "http://服务器ip:端口/coupon/create" with open(r"D:\Python\pycharm\API-TESTS\mall\test_cases\coupon_module\test_data.json", mode="rt", encoding="utf8") as x: # 打开json文件 result = json.load(x) # 把json文件转化成对应格式的列表或者字典 payload = json.dumps(result[3][0]) headers = { 'Authorization': f'Bearer {sccess_token}', 'Content-Type': 'application/json' } response = requests.post(url=url, headers=headers, data=payload) # =================断言======================== assert response.status_code == result[3][1]["code"], "用例测试不通过" def test_coupon5(self,sccess_token): url = "http://服务器ip:端口/coupon/create" with open(r"D:\Python\pycharm\API-TESTS\mall\test_cases\coupon_module\test_data.json", mode="rt", encoding="utf8") as x: # 打开json文件 result = json.load(x) # 把json文件转化成对应格式的列表或者字典 payload = json.dumps(result[4][0]) headers = { 'Authorization': f'Bearer {sccess_token}', 'Content-Type': 'application/json' } response = requests.post(url=url, headers=headers, data=payload) # =================断言======================== assert response.status_code == result[4][1]["code"], "用例测试不通过"
test_cases----coupon_module-----test_data.json
[[{ "amount": 2, "enableTime":null, "endTime": null, "minPoint": 4, "name": "优惠券1", "note": null, "perLimit": "3", "platform": 0, "productCategoryRelationList": [], "productRelationList": [], "publishCount": 1, "startTime": null, "type": 0, "useType": 0 },{"code":200}], [{ "amount": 2, "enableTime":null, "endTime": null, "minPoint": 4, "name": "优惠券2", "note": null, "perLimit": "3", "platform": 0, "productCategoryRelationList": [], "productRelationList": [], "publishCount": -1, "startTime": null, "type": 0, "useType": 0 },{"code":403}], [{ "amount": -2, "enableTime":null, "endTime": null, "minPoint": 4, "name": "优惠券3", "note": null, "perLimit": "3", "platform": 0, "productCategoryRelationList": [], "productRelationList": [], "publishCount": 1, "startTime": null, "type": 0, "useType": 0 },{"code":403}], [{ "amount": 2, "enableTime":null, "endTime": null, "minPoint": 4, "name": "优惠券4", "note": null, "perLimit": "-3", "platform": 0, "productCategoryRelationList": [], "productRelationList": [], "publishCount": 1, "startTime": null, "type": 0, "useType": 0 },{"code":403}], [{ "amount": 2, "enableTime":null, "endTime": null, "minPoint": -4, "name": "优惠券5", "note": null, "perLimit": "3", "platform": 0, "productCategoryRelationList": [], "productRelationList": [], "publishCount": 1, "startTime": null, "type": 0, "useType": 0 },{"code":403}] ]
test_cases----login_module-----test_login.py
import pytest import requests def test_login(): url = "http://服务器ip:端口/admin/login" payload = { "password": "macro123", "username": "admin" } # res=requests.post(url=url,json=payload).json() #获取HTTP中响应body的json文本,自动的帮助转化成python对象 res = requests.post(url=url, json=payload) # print(type(res))#查看类型 #print(res) #===============断言===================== assert res.status_code==200,"用例测试不通过"
test_cases----productquery_module-----test_product.py
import pytest import requests #=============商品查询============================= def test_productchaxun(sccess_token): url = "http://服务器ip:端口/productCategory/list/0" payload ={ "pageNum":1, "pageSize":500 } headers = { 'Authorization': f'Bearer {sccess_token}' } expect_value='服装' response = requests.get(url=url, headers=headers, params=payload) #=================断言======================= assert expect_value in response.text,"用例测试不通过"