pytest自动化测试框架(基础篇)

测试用例发现规则

1、模块名必须以test_开头或者_test结尾;

2、测试类必须以Test开头,并且不能带有init方法;

3、测试用例必须以test_开头;

print("hello python!")

pytest常用插件

pytest

pytest-html

pytest-xdist

pytest-rerunfailures

pytest-ordering

allure-pytest

pyyaml

requests

写入requrtments.txt文件中

pip install -r requrtments.txt

运行参数及其配置化

Pytest -h :获取帮助信息

Pytest -v:显示详细信息

Pytest -s:关闭输出捕获

Pytest -n X:使用X个进程并行化执行用例

Pytest -n auto:自动选进程数执行

Pytest --html=Path:生成HTML测试报告,并保持在Path路径

Pytest --self-contained-html:HTML文件

Pytest -reruns X:测试用例失败后,重试X次

Pytest 执行方式

1、命令行方式

2、main函数方式

import pytest

if __name__ == '__main__':
    pytest.main()

3、pytest.ini执行方式

[pytest]
#参数
addopts = -sv --html=report/report.html

pytest前后置固件

setup_method():每个用例执行前执行一次

teardown_method():每个用例执行后执行一次

setup_class():每个类执行前执行一次

teardown_class():每个类执行后执行一次

使用fixture实现部分前后置

@pytest.fixture()

@pytest.fixture(scope="function", autouse=True)
def check_sql():
    print("执行检查sql方法")
    # 测试用例执行后执行
    yield
    print("关闭数据库")

1、scope:作用域:

​ function:在函数之前和之后执行;

​ --手动调用方式:在测试用例参数里面加上fixture的名称;

​ --fixture的返回值可以传递到测试用例中,通过fixture名称方式传递;

问题:测试类继承TestCase,测试用例调用fxiture执行报错

@pytest.fixture(scope="function")
def check_sql():
    print("执行检查sql方法", end='\n')
    yield
    print("关闭数据库", end='\n')

class TestUi(TestCase):

    def test_01(self, check_sql):
        print("test_01执行中", end='\n')

    def test_02(self,check_sql):
        print("test_02执行中", end='\n')

    def test_03(self):
        print("test_03执行中", end='\n')

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

取消继承TestCase后,重新执行正常

​ class:在类之前和之后执行;

​ --手动调用方式:在类上面加上@pytest.mark.usefixtures(“fixture名称”)装饰器调用;

@pytest.fixture(scope="class")
def check_sql():
    print("执行检查sql方法", end='\n')
    yield True
    print("关闭数据库", end='\n')

@pytest.mark.usefixtures("check_sql")
class TestUi:

    def test_01(self):
        print("test_01执行中", end='\n')

    def test_02(self):
        print("test_02执行中", end='\n')

    def test_03(self):
        print("test_03执行中", end='\n')

​ module:

​ package:

​ session:在整个项目会话之前和之后执行

2、autouse:自动调用fixture,默认为False

3、params:实现参数化

​ 通过fixture函数的参数里面加入request来接收参数,然后通过request.param来取值;

@pytest.fixture(scope="function", name='cs', params=['data1','data2','data3'])
def check_sql(request):
    print("执行检查sql方法", end='\n')
    print(f"检查{request.param}")
    yield True
    print("关闭数据库", end='\n')


@pytest.mark.usefixtures("cs")
class TestUi:

    def test_01(self):
        print("test_01执行中", end='\n')

    def test_02(self):
        print("test_02执行中", end='\n')

    def test_03(self):
        print("test_03执行中", end='\n') 

4、ids:不能单独使用,必须和params一起使用,作用是对参数起别名;

sql_data = ['data1', 'data2', 'data3']
@pytest.fixture(scope="function", name='cs', params=sql_data, ids=['1','2','3'])
def check_sql(request):
    print("执行检查sql方法", end='\n')
    print(f"检查{request.param}")
    yield True
    print("关闭数据库", end='\n')


@pytest.mark.usefixtures("cs")
class TestUi:

    def test_01(self):
        print("test_01执行中", end='\n')

    def test_02(self):
        print("test_02执行中", end='\n')

    def test_03(self):
        print("test_03执行中", end='\n')

执行结果:

test_case/test_ui.py::TestUi::test_01[1] 执行检查sql方法
检查data1
test_01执行中
PASSED关闭数据库

test_case/test_ui.py::TestUi::test_01[2] 执行检查sql方法
检查data2
test_01执行中
PASSED关闭数据库

test_case/test_ui.py::TestUi::test_01[3] 执行检查sql方法
检查data3

5、name:作用是给fixture起别名;一旦使用了别名,那么fixture的的名称就失效了,只能用别名;

fixture结合conftest.py文件使用

1、conftest.py专门用于存放fixture的配置文件,文件名是固定的;

2、在conftest.py文件里面所有的方法在调用时都不需要导包;

3、conftest.py文件可以有多个,并且多个conftest.py文件里面的多个fixture可以被一个测试用例使用;

setup_method,teardown_method,setup_class,teardown_class,fixture,conftest优先级

会话:fixture的session级别优先级最高;session级的fixture永远最先执行

如果fixture的autouse=True:

​ 类:setup_class比class级别的fixture后执行;

​ 函数:setup_method比function级别的fixture后执行;

如果fixture的autouse=Flase且使用@pytest.mark.usefixtures(“fixture名称”)装饰测试类:

​ 类:setup_class比class级别的fixture先执行;

​ 函数:function级别的fixture比setup_method先执行;

以上执行顺序是可以反转的(自学 不推荐反转)

官方建议fixture优先执行,理由如下:

​ Pytest 将 fixture 视为测试的“依赖项”,需要在测试方法(及其传统的 setup/teardown)之前准备好。

​ Fixture 可以跨测试类共享,而 setup_method 仅限于当前类。

实际应用建议:

​ 1、优先使用fixture,现代 Pytest 推荐完全用 fixture 替代 setup_method

​ 2、避免混用,混合使用 setup_method 和 fixture 可能导致执行顺序混乱,建议统一风格。

总结:pytest执行过程

1、查询当前目录下的conftest.py文件;

2、查询当前目录下的pytest.ini文件;

3、查询用例目录下的conftest.py文件;

4、查询测试用例的py文件中是否有setup,teardown,setup_class,teardown_class;

5、执行用例;

pytest断言

使用python自己的断言:assert

pytest结合allure-pytest插件生成美观的报告

1、安装allure-pytest插件;

pip install allure-pytest

2、下载allure,下载之后解压,解压之后再配置环境变量(把allure目录下的bin目录配置到系统变量path路径中);

​ allure压缩包下载路径:https://github.com/allure-framework/allure2/releases

​ 解压软件包(建议直接放到Python文件夹下),然后添加bin目录到环境变量中

​ 验证安装成功 CMD窗口输入:allure --version

PS C:\Users\10997> allure --version
2.33.0

注意:PC需要提前安装javaSDK

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

验证java环境安装成功:

PS C:\Users\10997> Java -version
java version "20.0.2" 2023-07-18
Java(TM) SE Runtime Environment (build 20.0.2+9-78)
Java HotSpot(TM) 64-Bit Server VM (build 20.0.2+9-78, mixed mode, sharing)

3、生成allure报告

​ 先生成临时json报告,在pytest.ini文件中加入以下内容:

​ addopts = --alluredir=./temps --clean-alluredir

addopts = --alluredir=./temps --clean-alluredir
addopts = --alluredir=./tmeps --clean-alluredir

​ --alluredir=./temps:生成临时报告

​ --clean-alluredir:清空临时报告

​ 生成allure正式报告:

if __name__ == '__main__':
	pytest.main()
    time.sleep(3)
    os.system("allure generate ./temps -o ./reports --clean")

4、allure报告定制化(自学)

pytest之parametrize()实现数据驱动

@pytest.mark.parametrize(args_name,args_value)

@pytest.mark.parametrize(args_name, )

args_name:参数名称,用于将参数值传递给函数;

args_value:参数值;列表或者元组,有n个元素,用例将执行n次

第一种用法:

class TestUi(Suit01):
    @pytest.mark.parametrize("data", ["1", "2", "3"])
    def test_01(self, data):
        print("test_01执行中", end='\n')
        print(f"{data=}", end='\n')

执行结果如下:

testcases/test_ui.py::TestUi::test_01[1] setup_class 执行中
执行检查sql方法
setup_method 执行中
test_01执行中
data='1'
PASSEDteardown_method 执行中
关闭数据库

testcases/test_ui.py::TestUi::test_01[2] 执行检查sql方法
setup_method 执行中
test_01执行中
data='2'
PASSEDteardown_method 执行中
关闭数据库

testcases/test_ui.py::TestUi::test_01[3] 执行检查sql方法
setup_method 执行中
test_01执行中
data='3'
PASSEDteardown_method 执行中
关闭数据库
@pytest.mark.parametrize("data", (["1","2"], {"姓名":"黄浩"}, "3"))

第二种用法:

class TestUi(Suit01):
    @pytest.mark.parametrize("data1,data2", (["香蕉", "牛奶"], ["苹果", "醋"], ["橘子", "水"]))
    def test_01(self, data1, data2):
        print("test_01执行中", end='\n')
        print(f"{data1=}", end='\n')
        print(f"{data2=}", end='\n')

执行结果如下:

testcases/test_ui.py::TestUi::test_01[\u9999\u8549-\u725b\u5976] setup_class 执行中
执行检查sql方法
setup_method 执行中
test_01执行中
data1='香蕉'
data2='牛奶'
PASSEDteardown_method 执行中
关闭数据库

testcases/test_ui.py::TestUi::test_01[\u82f9\u679c-\u918b] 执行检查sql方法
setup_method 执行中
test_01执行中
data1='苹果'
data2='醋'
PASSEDteardown_method 执行中
关闭数据库

testcases/test_ui.py::TestUi::test_01[\u6a58\u5b50-\u6c34] 执行检查sql方法
setup_method 执行中
test_01执行中
data1='橘子'
data2='水'
PASSEDteardown_method 执行中
关闭数据库

思考:与fixture的params区别

@pytest.fixture(scope="function", name='cs', params=sql_data, ids=['1', '2', '3'])
@pytest.mark.parametrize("data", ["1", "2", "3"])

YAML格式测试用例

1、yaml是一种数据格式,扩展名是yaml/yml,支持#注释,通过缩进表示层级,区分大小写;

用途:

​ -做配置文件

​ -编写自动化测试用例

2、数据组成:

​ map对象,健:(空格)值

​ name: 黄浩

​ 数组,使用"-"表示列表

testcases:
	-test_01.py
	-test_02.py
	-test_03.py

parametrize结合yaml实现接口自动化测试

读取yaml文件

-
  name: 获取统一的接口鉴权码
  request:
    method: get
    url: https://api.weixin.qq.com/cgi-bin/token
    data:
      grant_type: client_credential
      appid: wx6b11b3efd1cdc290
      secret: 106a9c6157c4db5f6029918738f9529d
  validate: None
ROOT_DIR = Path(__file__).parent.parent
def read_yaml(yamlpath):
    with open(str(ROOT_DIR) + '/' + yamlpath, mode='r', encoding='utf-8') as f:
        value = yaml.load(stream=f, Loader=yaml.FullLoader)
        return value

测试用例实现参数化

-
  name: 获取统一的接口鉴权码1
  request:
    method: get
    url: https://api.weixin.qq.com/cgi-bin/token
    data:
      grant_type: client_credential
      appid: wx6b11b3efd1cdc290
      secret: 106a9c6157c4db5f6029918738f9529d
  validate: None
class TestApi(Suit01):
    session = requests.Session()
    access_token = ''
    @pytest.mark.parametrize('caseinfo', read_yaml('testcases/get_token.yaml'))
    def test_get_token(self, caseinfo):
        print('test_get_token 执行中')
        print(f"{caseinfo=}")
        # name = caseinfo['name']
        method = caseinfo['request']['method']
        url = caseinfo['request']['url']
        data = caseinfo['request']['data']
        # validate = caseinfo['validate']

        result = TestApi.session.request(method=method, url=url, params=data).json()
        TestApi.access_token = result['access_token']
        assert 'access_token' in result
-
  name: 编辑标签接口1
  request:
    method: post
    url: https://api.weixin.qq.com/cgi-bin/tags/update?access_token=
    data: { "tag": { "id": 134,"name": "码尚教育66666" } }
  validate: None
@pytest.mark.parametrize('caseinfo', read_yaml('testcases/adit_flag.yaml'))
def test_edit_flag(self, caseinfo):
    print('test_edit_flag 执行中')
    print(f"{caseinfo=}")
    # name = caseinfo['name']
    method = caseinfo['request']['method']
    url = caseinfo['request']['url'] + TestApi.access_token
    data = caseinfo['request']['data']
    # validate = caseinfo['validate']

    result = TestApi.session.request(method=method, url=url, json=data).json()
    print(f"{result=}")
    # result = {'errcode': 0, 'errmsg': 'ok'}
    assert result['errcode'] == 0 and result['errmsg'] == 'ok'

若请求类型为:get,则request的data参数名称为params;若请求类型为:post,则request的data参数名称为json或者其它;

封装统一接口请求

创建requests_util.py文件

class RequestsUtil:
    session = requests.Session()

    def send_request(self, method, url, data, **kwargs):
        result = ''
        method = str(method).lower()  # 设置字母全小写
        if method == 'get':
            result = RequestsUtil.session.request(method=method, url=url, params=data)
        elif method == 'post':
            result = RequestsUtil.session.request(method=method, url=url, json=data)
        else:
            pass
        return result

封装接口自动化测试框架(大成)

框架一出,任何项目都能用

要解决的问题:

1、yaml文件如何实现接口关联封装;

2、yaml文件如何实现动态参数的处理;

3、yaml文件如何实现文件上传;

4、yaml文件如何实现断言,特别是有参数化的时候;

5、yaml文件数据量太大咋办;

6、接口自动化框架的扩展:加密接口,签名接口,自定义的功能接口;

接口测试

http协议

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

TCP3次握手建立可靠连接:

  1. 第一次握手
    客户端向服务器发送一个 SYN(Synchronize Sequence Numbers)包,表示希望与服务器建立连接并随机生成初始序列号 Seq = X。此阶段客户端处于 SYN_SENT 状态1。
  2. 第二次握手
    服务器接收到客户端的 SYN 包后,会返回一个 SYN+ACK 包作为应答。该包包含两个部分:一是确认序号 Ack = X + 1 表示已接收到来自客户端的第一个包;二是服务器也随机生成自己的初始序列号 Seq = Y。此时服务器进入 SYN_RCVD 状态1。
  3. 第三次握手
    客户端再次发送 ACK 包给服务器,其中携带的是对服务器 SYN 包的确认信息 Ack = Y + 1 和自身的序列号 Seq = X + 1。一旦服务器接收到这个 ACK 包,则表明双方均已准备好进行数据传输,整个连接正式建立,状态变为 ESTABLISHED1。

http请求方式

get:查询

post:新增

put:修改

delete:删除

http状态码

1XX

2XX

3XX

4XX

5XX

接口自动化(细节篇)

全面熟练requests库以及底层方法调用逻辑

requests请求部分:

def get(url, params=None, ** kwargs):
url:接口地址
params:参数,在get请求的ur1后面传的参数
** kwargs:可变长度的字典
def post(url, data=None, json=None, ** kwargs):
ur1:接口地址
data:参数(表单 --- x-www-form-urlencoded)
json:参数(JSON --- application/json)
Postman四种传参方式:
1.form-data(既有表单也有文件上传)  files
2.x-www-form-urlencoded(纯表单)  data
3.raw(json:application/json)(传json格式的参数)json
4.binary(application/octrent-stream):(二进制文件)data
** kwargs:可变长度的字典
def put(url, data=None, ** kwargs):
ur1:接口地址
data:参数
def delete(url, ** kwargs):
ur1:接口地址

def get(url, params=None, ** kwargs):
def post(url, data=None, json=None, ** kwargs):
def put(url, data=None, ** kwargs):
def delete(url, ** kwargs):

上面的四个方法其实底层都是调用的request方法
requests.request("get", url, params=params, ** kwargs)
data=None,
requests.request()调用的是session.request方法
session.request(method=method, url=url, ** kwargs)

def request(
self,
method,  请求方式
url,  请求路径
params=None, params参数
data=None,data参数
headers=None,请求头
cookies=None, cookiests信息
files=None,文件上传
auto=None,鉴权
timeout=None,
allow_redirects=True,
proxies=None,
hooks=None,
stream=None,
verify=None,
cert=None,
json=None,):

requests.request()和session.request()的区别:前者每个请求都是独立的,后者会自动关联所有请求的cookie信息;

requests响应部分:

res.text      返回字符串形式的结果
res.json()    返回字典形式的结果
res.content   返回字节类型的结果
res.status_code 返回状态码
res.reason    返回状态信息
res.cookies    返回cookie信息
res.encoding  返回编码格式
res.headers   返回响应头
res.request.xxx  得到请求信息

requests接口自动化测试实战

接口关联三个层次:

Ⅰ级–通过类变量保存中间变量,实现接口关联;

class EstApi:
    access_token = ""
    csrf_token = ""
    # session = sessions.Session()
    sess = requests.session()
    # 获取access_token鉴权码接口
    def test_get_token(self):
        urls = "https://api.weixin.qq.com/cgi-bin/token"
        datas = {
            "grant_type": "client_credential",
            "appid": "wx8a9de038e93f77ab",
            "secret": "8326fc915928dee3165720c910effb86"
        }
        # res = requests.get(url=urls, params=datas)
        res = EstApi.sess.request(method='get', url=urls, params=datas)
        # 使用jsonpath() 提取access_token的值,返回列表
        lst = jsonpath.jsonpath(res.json(), "$.access_token")
        EstApi.access_token = lst[0]
        # access_token = res.json()['access_token']
        print(f"{lst=}")

    # 查询标签接口
    def test_select_flag(self):
        urls = "https://api.weixin.qq.com/cgi-bin/tags/get"
        datas = {
            "access_token": EstApi.access_token
        }
        res = EstApi.sess.request(method='get', url=urls, params=datas)
        print(f"{res.json()=}")

    # 编辑标签接口
    def test_edit_flag(self):
        urls = "https://api.weixin.qq.com/cgi-bin/tags/update?access_token=" + EstApi.access_token
        # datas1 = {
        #     "access_token": EstApi.access_token
        # }
        datas2 = {"tag": {"id": 134, "name": "A"}}
        res = EstApi.sess.request(method='post', url=urls, json=datas2)
        print(f"{res.json()=}")

    # 文件上传接口
    def test_file_upload(self):
        urls = "https://api.weixin.qq.com/cgi-bin/media/uploadimg"
        datas1 = {
            "access_token": EstApi.access_token
        }
        datas2 = {
            "media": open("C:\\Users\\10997\PycharmProjects\\api_pytest\\tupian.png", mode='rb')
        }
        res = EstApi.sess.request(method='post', url=urls, params=datas1, files=datas2)
        print(f"{res.json()=}")

    # 访问phpwind首页接口
    def test_phpwind(self):
        urls = "http://47.107.116.139/phpwind/"
        res = EstApi.sess.request(method='get', url=urls)
        # print(f"{res.text=}")
        # 使用正则表达式匹配字符
        EstApi.csrf_token = re.search('name="csrf_token" value="(.*?)"', res.text).group(1)
        print(f"{EstApi.csrf_token=}")

    # 登录接口
    def test_login(self):
        urls = "http://47.107.116.139/phpwind/index.php?m=u&c=login&a=dorun"
        headers = {
            "Accept": "application/json, text/javascript, /; q=0.01",
            "X-Requested-With": "XMLHttpRequest"
        }
        datas = {
        "username": "hh12345",
        "password": "hh12345",
        "csrf_token": EstApi.csrf_token,
        "backurl": "http://47.107.116.139/phpwind/",
        "invite": ""
        }
        res = EstApi.sess.request(method='post', url=urls, data=datas, headers=headers)
        print(f"{res.json()=}")

Ⅱ级–通过单独的文件保存中间变量,实现接口关联;

创建requests.util.py文件

class RequestsUtil:
    sess = requests.session()

    def send_all_request(self, **kwargs):
        return RequestsUtil.sess.request(**kwargs)

Ⅲ级–极限封装成零代码的方式实现接口关联;!!!

接口关联两种方式:

1、正则提取实现接口关联:

re.search() # 通过正则匹配一个值,通过下标【1】取值,没有找到返回None;

re.findall() # 通过正则匹配多个值,返回list,通过下标取值,没有找到返回None;

2、JsonPath提取实现接口关联:

jsonpath.jsonpath() # 返回一个列表,通过下标取值,没有找到返回None;

接口自动化测试框架的封装

1、去重冗余代码;

2、实现统一异常处理和日志监控;

引入用例管理框架pytest

作用:

1、发现用例:默认发现用例的规则

​ 模块名必须以test_开头或者以*_test.py结尾;

​ 测试类必须以Test开头;

​ 测试方法必须以test_开头;

2、执行用例

3、判断结果

4、生成测试报告

pytest用例管理框架详细介绍

特点:

1、结合selenium,requests,appium实现web,接口,app自动化;

2、结合allure生成非常美观的报告以及和Jenkins实现持续集成;

3、拥有很多强大的插件:

pytest
pytest-html          # 生成HTML报告
pytest-xdist         # 并行执行用例
pytest-rerunfailures # 重复执行失败用例
pytest-ordering      # 用例排序
pytest-base-url      # 基础路径
allure-pytest        # 生成allure测试报告

把这些放到requirements.txt里面,然后通过以下命令安装:

pip install -r requirements.txt

pytest用例管理框架执行方式

1、命令行运行

pytest

2、主函数运行

import pytest

if __name__ == '__main__':
    pytest.main()

3、通过配置文件pytest.ini来改变以及执行用例;

[pytest]
# 配置参数
addopts = -vs -m "smoke or user"
# 改变用例的查找规则
testpaths = ./testcases1 ./testcases2
# 改变模块的查找规则
python_files = test_*.py
# 改变类的查找规则
python_classes = Test*
# 改变函数的查找规则
python_functions = test_*
# 标记
markers = 
	smoke:冒烟用例
    user:用户管理用例

不管是命令行还是主函数都会读取pytest.ini文件来执行

pytest用例管理框架的前后置(固件/夹具)

def setup():
    print('每个用例之前执行一次')

def teardown():
    print('每个用例之后执行一次')
    
def setup_class():
    print('每个类之前执行一次')
    
def teardown_class():
    print('每个类之后执行一次')

更强大的前后置:fixture

装饰器:

@pytest.fixture(scope="作用域",params="参数化",autouse="自动执行",idl="参数别名",name="别名")
scope: function class module package session
params: 参数化["mysql","redis"] 需要使用request参数和yield/return request.param
autouse: True和False
idl: 参数别名
name: 夹具别名:用了夹具别名,夹具原名就会失效

@pytest.fixture(scope="function", autouse=True, params=['mysql','redis'], ids=["m","r"])
def check_data(request):
    print("打开数据库")
    yield request.param
    print("关闭数据库")

pytest接口关联封装(基于一个独立的文件)

创建yaml_util.py文件

# 写入
def write_yaml(data):
    with open(os.getcwd() + '/extract.yaml', mode='a+', encoding='utf-8') as f:
        yaml.dump(data, stream=f, allow_unicode=True)
        
# 读取
def read_yaml(key):
    with open(os.getcwd() + '/extract.yaml', mode='r', encoding='utf-8') as f:
        value = yaml.load(stream=f, Loader=yaml.FullLoader)
        return value[key]
    
# 清空
def clear_yaml():
    with open(os.getcwd() + '/extract.yaml', mode='w', encoding='utf-8') as f:
        # 清空文件
        f.truncate()

YAML详细介绍

键值对 键:(空格)值

列表 -

yaml用例格式:

-
  feature: 模块
  story: 接口
  title: 用例标题
  request:
    method: 请求方式
    url: 请求路径
    headers: 请求头
    data: 请求参数
  vilidate: 断言

数据驱动

@pytest.mark.paramtrize(“参数名”,“值”)

创建test_api.yaml文件

-
  feature: test_api
  story: get_token
  title: 获取正确的鉴权码
  request:
    method: get
    url: "https://api.weixin.qq.com/cgi-bin/token"
    headers: null
    params:
      "grant_type": "client_credential"
      "appid": "wx8a9de038e93f77ab"
      "secret": "8326fc915928dee3165720c910effb86"
  vilidate: null

修改测试用例为:

@pytest.mark.parametrize("testinfo", read_yaml_testcase('testcases/test_api.yaml'))
def test_get_token(self, testinfo):
    methods = testinfo['request']['method']
    urls = testinfo['request']['url']
    datas = testinfo['request']['params']
    res = RequestsUtil().send_all_request(method=methods, url=urls, params=datas)
    # 使用jsonpath() 提取access_token的值,返回列表
    lst = jsonpath.jsonpath(res.json(), "$.access_token")
    TestApi.access_token = lst[0]
    # 将access_token值保存到extract.yaml文件中
    write_yaml({"access_token": lst[0]})
class TestApi:

    # 获取access_token鉴权码接口
    @pytest.mark.parametrize("testinfo", read_yaml_testcase('testcases/test_get_token.yaml'))
    def test_get_token(self, testinfo):
        methods = testinfo['request']['method']
        urls = testinfo['request']['url']
        datas = testinfo['request']['params']
        res = RequestsUtil().send_all_request(method=methods, url=urls, params=datas)
        print(res.json())
        # 使用jsonpath() 提取access_token的值,返回列表
        lst = jsonpath.jsonpath(res.json(), "$.access_token")
        TestApi.access_token = lst[0]
        # 将access_token值保存到extract.yaml文件中
        write_yaml({"access_token": lst[0]})

    # 编辑标签接口
    @pytest.mark.parametrize("testinfo", read_yaml_testcase('testcases/test_edit_flag.yaml'))
    def test_edit_flag(self, testinfo):
        methods = testinfo['request']['method']
        urls = testinfo['request']['url'] + read_yaml("access_token")
        datas2 = testinfo['request']['json']
        res = RequestsUtil().send_all_request(method=methods, url=urls, json=datas2)
        print(f"{res.json()=}")

    # 查询标签接口
    @pytest.mark.parametrize("testinfo", read_yaml_testcase('testcases/test_select_flag.yaml'))
    def test_select_flag(self, testinfo):
        methods = testinfo['request']['method']
        urls = testinfo['request']['url']
        datas = testinfo['request']['params']
        datas['access_token'] = read_yaml('access_token')
        res = RequestsUtil().send_all_request(method=methods, url=urls, params=datas)
        print(f"{res.json()=}")

    # 文件上传接口
    @pytest.mark.parametrize("testinfo", read_yaml_testcase('testcases/test_file_upload.yaml'))
    def test_file_upload(self, testinfo):
        methods = testinfo['request']['method']
        urls = testinfo['request']['url']
        datas1 = testinfo['request']['params']
        datas1['access_token'] = read_yaml('access_token')
        datas2 = testinfo['request']['files']
        datas2['media'] = open(datas2['media'], mode='rb')
        res = RequestsUtil().send_all_request(method=methods, url=urls, params=datas1, files=datas2)
        print(f"{res.json()=}")

问题思考:

1、如何在yaml中调用随机数方法;

2、一个接口对应一个yaml,yaml数据多了怎么处置;

3、yaml中如何断言;

allure报告

1、下载allure,配置allure环境变量

2、生成临时json报告

addopts = -vs --alluredir=./temps --clean-alluredir

3、生成allure测试报告

os.system("allure generate ./temps -o ./reports --clean")

web自动化(细节篇)

自动化测试的误区

1、自动化测试找不到bug

2、完全用不到自动化测试

3、有了自动化测试,就不用手工测试了

简易自动化价值或收益(ROI)

ROI = (手工测试成本*迭代次数)— 第一次自动化成本 —(自动化脚本维护*迭代成本)

适合做自动化测试的项目

适合项目:

1、产品型项目:功能是增量开发,回归测试的用例量大,存在大量重复的用例

不适合项目:

1、定制版项目:项目周期短,业务很复杂,项目需求不稳定,涉及物理交互

自动化测试的介入点

项目的中后期或者维护期;

自动化覆盖率(自动化测试用例和手工测试用例的占比)

RTN分组自动化覆盖率:87%

自动化实施的过程

1、可行性分析

2、框架选择(selenium,RF)

3、需求分析

4、制定计划

5、测试用例的设计

6、环境搭建,框架搭建

7、版本控制,持续集成,无人值守,报告生成

8、脚本维护

python+selenium环境搭建

待补齐

元素定位

元素定位失败分析:

1、元素是否加载完成?

2、页面是否存在frame和iframe?

3、元素是否不可用,不可读,不可见?

4、是否是动态元素,动态DIV层,自定义控件?

前提:元素必须唯一

八种定位方式:

id

driver.find_element(By.ID, "kw").send_keys("西昌学院")

name

driver.find_element(By.NAME, "wd").send_keys("四川大学")

xpath

# 绝对路径 用一个 /开头
driver.find_element(By.XPATH, "/html/body/div/div[2]/div[5]/div[1]/div/form/span[1]/input").send_keys("西昌学院")
# 相对路径 用 //开头
driver.find_element(By.XPATH, "//span/input").send_keys("西昌学院")
# 相对路径 + 索引
driver.find_element(By.XPATH, "//span[1]/input").send_keys("西昌学院")
# 相对路径 + 属性
# <input type="text" class="s_ipt" name="wd" id="kw" maxlength="100" autocomplete="off" placeholder="印度男子不慎摔入余烬堆身亡">
driver.find_element(By.XPATH, "//input[@class='s_ipt']").send_keys("西昌学院")
driver.find_element(By.XPATH, '//input[@autocomplete="off"]').send_keys("西昌学院")
# 相对路径 + 部分属性
	# 属性值的开头
driver.find_element(By.XPATH, '//input[starts-with(@class, "s")]').send_keys("西昌学院")
	# 属性值的结尾
driver.find_element(By.XPATH, '//input[substring(@class,3)="ipt"]').send_keys("西昌学院")
	# 属性值的包含
driver.find_element(By.XPATH, '//input[contains(@autocomplete,"ff")]').send_keys("西昌学院")
	# 相对路径 + 通配符
driver.find_element(By.XPATH,'//*[@autocomplete="off"]').send_keys("西昌学院")
driver.find_element(By.XPATH,'//*[@*="off"]').send_keys("西昌学院")
    # 相对路径 + 文本  <span class="soutu-hover-tip" style="display: none;">按图片搜索</span>
driver.find_element(By.XPATH, '//span[text()="按图片搜索"]').get_attribute('class')

css 与xpath相似

略

link_text

driver.find_element(By.LINK_TEXT, "新闻").click()

partial_link_text

driver.find_element(By.PARTIAL_LINK_TEXT, "hao").click()

class_name

基本不用

tag_name

基本不用

项目实战

# frame和iframe(框架)
# 进入框架
driver.switch_to.frame("框架名")
# 跳出框架
driver.switch_to.default_content()
# 处理下拉框
sel = Select(driver.find_element())
sel.select_by_index() # 下标
sel.select_by_value() # 值
sel.select_by_visible_text() # 文本

设计模式

1、PO:page object model 页面对象模型

# 基础层 base 主要放selenium原生的方法;
from selenium import webdriver
from selenium.webdriver.support.select import Select


class BasePage:

    def __init__(self, driver):
        self.driver = driver
        # self.driver = webdriver.Edge()
        # self.driver.implicitly_wait(5)

    # def open_browser(self):
    #     self.driver = webdriver.Edge()
    #     # 智能等待5min
    #     self.driver.implicitly_wait(5)
    # 打开网页
    def get(self, url):
        self.driver.get(url)

    # 定位元素
    def location_element(self, args: tuple):
        """传入元组数据,然后解包传给find_element"""
        return self.driver.find_element(*args)

    # 发送value给元素
    def send_keys(self, args, value):
        self.location_element(args).send_keys(value)

    # 单击元素
    def click(self, args):
        self.location_element(args).click()

    # 进入框架
    def goto_frame(self, frame_name):
        self.driver.switch_to.frame(frame_name)

    # 退出框架
    def out_frame(self):
        self.driver.switch_to.default_content()

    # 下拉框选择
    def choose_select(self, args, value):
        sel = Select(self.location_element(args))
        sel.select_by_value(value)
# 页面对象层 pageobject 继承基础层 主要放页面的元素和页面的动作 一个页面就是一个类
from selenium.webdriver.common.by import By

from base.base_page import BasePage


class LoginPage(BasePage):
    # 封装页面元素
    current_url = "http://localhost/signin"
    user_loc = (By.ID, "email")
    password_loc = (By.ID, "password")
    login_button_loc = (By.XPATH, '//button[text()="登录"]')

    # 封装页面动作
    def login_dify(self, username="1099706290@qq.com", password="@huang213"):
        # self.open_browser()
        self.get(self.current_url)
        self.send_keys(self.user_loc, username)
        self.send_keys(self.password_loc, password)
        self.click(self.login_button_loc)
# 测试用例层 testcase 调用pageabject 存放一个测试用例以及测试数据
class TestDify:
    driver = webdriver.Edge()
    driver.implicitly_wait(5)
    username = ""
    password = ""

    def test_login(self):
        lp = LoginPage(self.driver)
        lp.login_dify()
        sleep(2)

2、关键字驱动

略

项目实战

略

引入pytest

unittest测试框架(耦合)

1、新建一个类继承unittest.TestCase类

2、导入unittest

3、新建一个以test_开头的测试用例

pytest测试框架(非耦合)

1、模块名必须以test_开头或者_test结尾

2、测试类名必须以Test开头

3、测试函数必须以test_开头

conftest.py

import pytest
from selenium import webdriver

# 打开浏览器,一个测试类只执行一次
@pytest.fixture(scope="class", autouse=True)
def open_browser():
    print("打开浏览器")
    driver = webdriver.Edge()
    driver.implicitly_wait(5)
    yield driver
    print("关闭浏览器")

test_dify.py

class TestDify:
    username = ""
    password = ""

    def test_login(self, open_browser):
        lp = LoginPage(open_browser)
        lp.login_dify()
        sleep(2)

    def test_tansuo(self, open_browser):
        lp = LoginPage(open_browser)
        lp.login_dify()
        ts = TanSuo(open_browser)
        ts.tansuo()
        sleep(2)

生成企业级的allure报告

1、下载allure,并配置环境文件

2、下载allure-pytest插件

3、生成临时文件

opts = -vs --alluredir=./temps --clean-allure

4、生成allure报告

os.system("allure generate ./temps -o ./reports --clean")

报告定制:

​ 定制错误截图示例:

def test_login(self, open_browser):
    try:
        lp = LoginPage(open_browser)
        lp.login_dify()
        sleep(2)
        assert 1==2
    except Exception as e:
        # 保存错误截图
        file_path = "C:/Users/10997/PycharmProjects/WebPytest/error_log/login_error.png"
        open_browser.save_screenshot(file_path)
        # 把错误截图放到allure报告中
        with open(file_path, mode="rb") as f:
            allure.attach(body=f.read(), name="login_error.png", attachment_type=allure.attachment_type.PNG)

​

pytest实现数据驱动

pytest.mark.parametrize(args_name, args_value)
args_name:参数名
args_value:参数值(一般为list tuple 字典列表,字典元组等),列表里有多少个元素,用例就执行多少次

数据类型为excel,读取插件:xlrd openpyxl

excel_util.py

def read_excel(excel_url, sheet_name):
    # 加载工作簿
    wb = openpyxl.load_workbook(excel_url)
    # 获取sheet页对象
    sheet = wb[sheet_name]
    # 打印sheet最大的行数和列数
    print(sheet.max_row, sheet.max_column)
    # 读取excel数据,组成列表数据
    row_list = []
    for row in range(2, sheet.max_row + 1):
        col_list = []
        # print(sheet.cell(row, 5).value)
        if sheet.cell(row, 5).value == 'y':
            for col in range(1, sheet.max_column):
                col_list.append(sheet.cell(row, col).value)
            # print(col_list)
            row_list.append(col_list)
    return row_list

test_dify.py

class TestDify:

    @pytest.mark.parametrize("testinfo", read_excel(excel_url="./testcases/test_dify.xlsx", sheet_name="test_login"))
    def test_login(self, open_browser, testinfo):
        username = testinfo[2]
        password = testinfo[3]
        try:
            lp = LoginPage(open_browser)
            lp.login_dify(username, password)
            sleep(2)
            # assert 1==2
        except Exception as e:
            # 保存错误截图
            file_path = "C:/Users/10997/PycharmProjects/WebPytest/error_log/login_error.png"
            open_browser.save_screenshot(file_path)
            # 把错误截图放到allure报告中
            with open(file_path, mode="rb") as f:
                allure.attach(body=f.read(), name="login_error.png", attachment_type=allure.attachment_type.PNG)

jenkins持续集成及集成allure报告

1、下载allure插件(allure-jenkins-plugin)并且放到jinkins的plugins目录下

2、在jinkens中配置allure

3、在job中加入allure报告

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐