Python-接口自动化测试
一、接口测试
1、接口的概念
程序内部的接口:方法和方法间的交互
系统对外的接口:实现数据共享
2、接口测试
(1)概念
通过测试不同情况下的入参与之相应的出参信息来判断接口是否符合或满足相应的功能性、安全性要求。
(2)接口用例的编写
【用例编号、用例标题、前置条件、测试步骤、预期结果、实际结果、测试结果】
①通过性验证②参数组合③接口安全(接口参数伪造,越权、隐私数据安全)④异常验证
二、接口自动化测试
1、流程
需求分析——挑选自动化接口——设计自动化测试用例——搭建自动化测试框架——设计自动化框架——编写代码——执行用例——生成测试报告
2、requests模块
(1)功能
用Python代码模拟人手动发送接口请求,自动测接口(代替手动点Postman、填参数、点发送)
| Postman 手动操作 | Python requests 代码写法 |
|---|---|
| 填接口 URL | 写url = "地址" |
| 选请求方式 GET/POST | requests.get() / requests.post() |
| 填 Headers 请求头 | 字典写 headers |
| 填 Body 请求参数 | json/params 传参 |
| 点 Send 发送请求 | 执行这行代码就自动发送 |
| 看返回状态码、返回数据 | res.status_code、res.json() |
| 人工判断结果对不对 | 代码写assert自动判断(自动化核心) |
(2)常用需求
get、post、put、delete(需要先pip install requests)
!get和post底层都是由request实现
request可传递的参数:
| 参数名 | 作用 | 最常用场景 |
|---|---|---|
| method | 请求方式:GET/POST/PUT/DELETE | 必传,写字符串 "GET" |
| url | 接口地址 | 必传,接口网址 |
| params | URL 后面拼接的参数 ?wd=xxx |
GET 请求传参 |
| data | 表单格式传参(老式接口) | 很少用 |
| json | JSON 格式传参(现在主流) | 登录、提交数据 |
| headers | 请求头(放 token、浏览器标识) | 几乎所有接口都要 |
| cookies | 带 cookie 请求(保持登录) | 登录态接口 |
| timeout | 设置超时时间(秒) | 防止接口卡死 |
| files | 上传文件 | 文件上传接口 |
import requests
r=requests.get("https://www.baidu.com")
print(r.status_code)
print(r.text)
# print(r.json())
print(r.headers)
get=requests.get("https://www.baidu.com")
post=requests.post("https://www.baidu.com")
req_get=requests.request(method="GET",url="https://www.baidu.com");
req_post=requests.request("POST","https://www.baidu.com");
print("get:",get)
print("post:",post)
print("req_get:",req_get)
print("req_post:",req_post)
(3)响应结果
status_code——状态码
json()——返回字符串为字典
text——纯文本返回
(4)代码
import requests
#GET
url="https://www.baidu.com"
r=requests.request(url=url,method="GET")
print(f"status_code:{r.status_code}")
# print("json:"+r.json())
print("text:"+r.text)
#带参GET
url="https://www.baidu.com"
param={"wd":"Python"}
r=requests.request(url=url,method="GET",params=param)
print(f"status_code:{r.status_code}")
print("text:"+r.text)
#POST
url="https://www.baidu.com"
r=requests.request(url=url,method="POST")
print(f"status_code:{r.status_code}")
print("text:"+r.text)
3、自动化框架pytest
requests库专注于HTTP请求的发送,pytest框架提供了测试的组织、执行、管理功能
可以自动识别到测试用例,不需要再手动编写main函数并调用测试用例,Terminal直接运行命令
(1)规则
测试文件:文件名必须以 test_ 开头 或者 _test 结尾。例:test_api.py、login_test.py
测试函数:函数名必须以 test 开头。例:test_login()、test_baidu_search()
测试类:必须以Test开头,并且不能有__init__方法(非测试类可以有__init__方法)
以上三个规则必须全部遵守才能被pytest识别成测试用例!
(2)命令参数
| 命令 | 描述 |
| pytest | 在当前目录及其子目录中搜索并测试运行 |
| pytest -v | 增加输出的详细程度 |
| pytest -s | 显示测试中的print语句 |
| pytest test_module.py | 运行指定的测试模块 |
| pytest test_dir/ | 运行指定目录下的所有测试 |
(3)配置文件
使用配置文件后pytest会实现配置中的要求
| 参数 | 解释 |
| addopts | 指定在命令行中默认包含的选项 |
| testpaths | 指定搜索测试的目录 |
|
python_files |
指定发现测试模块时使用的文件匹配模型 |
| python_classes | 指定发现测试类时使用的类名的前缀或模式 |
| python_functions | 制定发现测试函数和方法时使用的函数名称前缀或模式 |
(4)前后置
①setup_method、teardown_method:用于类中每个测试方法的前置后置
②setup_class、teardown_class:用于整个测试类的前置后置
③fixture

(5)断言
语法:assert 条件表达式
①基本类型
def test_base():
a=1
b=1
assert a==b
str1="aaa"
str2="bbb"
assert str1!=str2
②数据结构
def test_ds():
#断言列表
expect_list=[1,'a',2,3]
actual_list=[1,'a',2,3]
assert expect_list==actual_list
#断言元组
expect_tuple=(1,'a',2,3)
actual_tuple=(1,'a',2,3)
assert expect_tuple==actual_tuple
#断言字典
expect_dict={'name':'aa','age':15}
actual_dict={'name':'aa','age':15}
assert expect_dict==actual_dict
#断言集合
expect_set={1,'a',2,3}
actual_set={1,'a',2,3}
assert expect_set==actual_set
③函数
#函数断言
def divide(a,b):
assert b!=0,"除数不能为0"
return a/b
def test_divide():
print(divide(10,0))

④断言接口
import requests
#断言接口,对所有内容进行校验
def test_interfaceAll():
url="http://jsonplaceholder.typicode.com/posts/1"
r=requests.request(method="GET",url=url)
# print(r.json())
expect_data={
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
actual_data=r.json()
assert expect_data==actual_data
#对关键字进行校验
def test_interfaceKey():
url="http://jsonplaceholder.typicode.com/comments?postId=1"
r=requests.get(url=url)
assert r.json()[0]['id']==1
assert r.json()[1]['id'] == 1

(6)参数化
pytest.mark.parametrize
import pytest
#统一的参数类型
@pytest.mark.parametrize("data",(1,2,3))
def test_unified(data):
print(data)
#参数类型不统一
@pytest.mark.parametrize("data",(1,"aaa",2.36,'m'))
def test_not_unified(data):
print(data)
#多个参数
@pytest.mark.parametrize("test_input,expected",[("1+1",2),("2*3",8),("4-3",0)])
def test_eval(test_input,expected):
assert eval(test_input)==expected
#在类上参数化
@pytest.mark.parametrize("n,expected",[(1,2),(2,3)])
class TestClass:
def test_simple_case(self,n,expected):
assert n+1==expected
def test_weird_simple_test(self,n,expected):
assert (n*1)+1==expected
#对多个类中的所有测试进行参数化,需要将pytestmark全局变量赋值
pytestmark=pytest.mark.parametrize("data",(1,2))
class Test_A:
def testA_01(self,data):
print(data)
def testA_02(self,data):
print("data")
class Test_B:
def testB_01(self,data):
print(data)
def testB_02(self,data):
print("data")
#接收其他函数的返回值作为参数
def data_provider():
return ["a","b","c"]
@pytest.mark.parametrize("data",data_provider())
def test_data(data):
print(data)
(7)fixture
提供测试函数所需的资源或上下文
import pytest
#fixture用法
@pytest.fixture
def fixture_01():
print("first fixture")
def test_01(fixture_01):
print("first case")
def test_02(fixture_01):
print("second case")
def test_03(fixture_01):
print("third case")
#fixture嵌套
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
order.append("b")
assert order==["a","b"]
#请求多个fixture
class Fruit:
def __init__(self,name):
self.name=name
def __eq__(self, other):
return self.name==other.name
@pytest.fixture
def my_fruit():
return Fruit("apple")
@pytest.fixture
def your_fruit(my_fruit):
return [my_fruit,Fruit("banana")]
def test_Fruit(my_fruit,your_fruit):
assert my_fruit in your_fruit
(8)yield fixture
yield fixture使用yield而不是return,该fixture的任何拆卸代码放置在yield之后。yield之前是前置代码,之后是后置代码。
yield什么都不返回:
import pytest
@pytest.fixture
def operator():
print("setup:initialize data")
yield
print("teardown:clean data")
def test_01(operator):
print("first test")

yield返回数据:
import pytest
@pytest.fixture()
def operator():
print("setup:initialize data")
yield 100
print("tearsown:clean data")
def test_02(operator):
print(100+operator)
assert 100==operator

import pytest
@pytest.fixture
def file_read():
print("打开文件句柄")
fo=open("test.txt","r",encoding="utf-8")
yield fo
print("关闭文件句柄")
fo.close()
@pytest.fixture
def file_write():
print("打开文件句柄")
fo = open("test.txt", "w", encoding="utf-8")
return fo
# yield fo
# print("关闭文件句柄")
# fo.close()
def test_file(file_read,file_write):
#写数据
w = file_write
w.write("我的法克呀小姐姐……")
w.close()
#读数据
r=file_read
str=r.read()
print(str)

(9)fixture带参数
@pytest.fixture( scope="function", params=None, autouse=False,ids=None, name=None )
| 参数名 | 可选值 | 默认值 | 核心作用 |
|---|---|---|---|
scope |
function/class/module/session |
function |
控制 fixture 的执行范围和生命周期,决定什么时候执行、执行几次 |
params |
列表 / 元组 / 可迭代对象 | None |
给 fixture 做参数化,自动生成多组测试用例,无需重复写用例代码 |
autouse |
True/False |
False |
设置 fixture 是否自动生效,无需在用例中手动传参调用 |
ids |
列表 / 元组 / 可调用对象 | None |
给 params 的每组参数设置自定义名称,大幅优化测试报告可读性 |
name |
字符串 | None |
给 fixture 设置自定义别名,替代原函数名 |
scope="funciton"(默认)
每个测试函数调用一次fixture
import pytest
#fixture带参数
@pytest.fixture(scope="function")
def fixture_01():
print("initialize")
yield
print("clean")
class TestCase:
def test_01(self,fixture_01):
print("test01")
def test_02(self,fixture_01):
print("test02")

scope="class"
在同一个测试类中,fixture只会在第一个测试函数开始前执行一次,在最后一个测试函数结束后执行一次
import pytest
#fixture带参数
@pytest.fixture(scope="class")
def fixture_01():
print("initialize")
yield
print("clean")
class TestCase01:
def test_01(self,fixture_01):
print("test01")
def test_02(self,fixture_01):
print("test02")
class TestCase02:
def test_01(self,fixture_01):
print("test01")
def test_02(self,fixture_01):
print("test02")

scope="module"
在同一个测试模块中共享fixture。当测试用例放在不同的文件时,需要将fixture放到配置文件(congtest.py)里,实现多个文件共享

scope="session"
整个测试会话中共享这个fixture

autouse
默认为False,若设置为True,则每个测试函数都会自动调用该fixture,无需显式传入(显式调用也不影响)
import pytest
@pytest.fixture(scope="session",autouse=True)
def fixture_01():
print("initialize")
yield
print("clean")

params参数化
import pytest
@pytest.fixture(params=[1,2,3])
def data_provider(request):
return request.param
def test_data(data_provider):
print(data_provider)

4、YAML
(1)概念
主要用于存储配置信息,不是编程语言
存储的是什么类型的数据,读出来就是什么类型的数据
注意:区分大小写,不允许使用制表符Tab键,严格缩进
(2)常用操作
| 操作 | 代码 |
| 读取 | yaml.safe_load(f) |
| 写入 | yaml.dump(f) |
| 清空 | yaml.truncate() |
import yaml
#往yaml中写入数据
def write_yaml(filename,data):
with open(file=filename,mode="a+",encoding="utf-8")as f:
yaml.safe_dump(data=data,stream=f)
#读取数据
def read_yaml(filename):
with open(file=filename,encoding="utf-8",mode="r")as f:
data=yaml.safe_load(stream=f)
return data
#清空数据
def clear_yaml(filename):
with open(file=filename,mode="w",encoding="utf-8")as f:
f.truncate()
def test_write():
data={
"name":"zhangsan",
"age":20
}
write_yaml("firstyaml.yml",data)
def test_read():
data=read_yaml("firstyaml.yml")
print(data)
def test_clear():
clear_yaml("firstyaml.yml")
(3)yaml转json格式
#读取数据
def read_yaml(filename):
with open(file=filename,encoding="utf-8",mode="r")as f:
data=yaml.safe_load(stream=f)
return data
def test_read():
data=read_yaml("secondyaml.yml")
print(json.dumps(data))

5、JSON Schema
(1)概念
是用来定义和校验JSON的web规范,校验json是否符合预期
接口返回值校验:①需要返回的字段是否都在②数据类型正确
用validate(instance=数据, schema规则)进行检验
from jsonschema.validators import validate
def test_01():
json_data = {
"name": "张三",
"age": 20,
"sex": "男"
}
json_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"sex": {"type": "string", "enum": ["男", "女"]}
},
"required": ["name", "age"]
}
validate(json_data,json_schema)

(2)数据类型
| 类型关键字 | 含义 |
|---|---|
| string | 字符串,表示文本数据 |
| integer | 整数 |
| number | 数字,表示浮点数/整数/…… |
| boolean | 布尔值 |
| object | 对象,用于嵌套的JSON对象 |
| array | 数组,表示列表或集合 |
| null | 空值 |
from jsonschema.validators import validate
def test_02():
json={
"data":[
{
"name":"zhangsan",
"age":"lisi"
},
{
"name":"lisi",
"age":18
}
],
"addr":None
}
json_schema={
"type":"object",
"properties":{
"data":{
"type":"array",
"properties":{
"name": {"type": "string"},
"age": {"type": "integer"}
}
},
"addr":{"type":"null"}
}
}
validate(json,json_schema)

(3)最大最小值
minimum,maximum(min<=x<=max)
exclusiveMinimum,exclusiveMaximum(min<x<max)
def test_03():
json_data = {
"name": "张三",
"age": 10
}
json_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer",
# "minimum":0,
# "maximum":100
"exclusiveMinimum": 0,
"exclusiveMaximum": 100
}
},
"required": []
}
validate(instance=json_data,schema=json_schema)
(4)字符串特殊校验
| 关键字 | 作用 | 示例 | 说明 |
|---|---|---|---|
| type: "string" | 定义是字符串 | "type": "string" |
必须加,代表这是字符串 |
| minLength | 最小长度 | "minLength": 2 |
字符长度 ≥ 2 |
| maxLength | 最大长度 | "maxLength": 10 |
字符长度 ≤ 10 |
| pattern | 正则表达式校验 | "pattern": "^[0-9]{11}$" |
手机号、邮箱、验证码 |
| enum | 只能是指定值 | "enum": ["男","女"] |
固定枚举字符串 |
| format | 格式校验 | "format": "email" |
邮箱、手机号、日期、URL |
可能需要用到正则表达式
| 符号 | 作用 |
|---|---|
^ |
开头 |
$ |
结尾 |
\d |
任意数字 |
\w |
字母 / 数字 / 下划线 |
[] |
匹配范围内字符 |
{n} |
固定 n 位 |
{m,n} |
m~n 位 |
+ |
至少 1 个 |
(5)数组约束
minItems,maxItems:指定数组的最大最小长度
uniqueItems:确保数组中的元素唯一
items:定义数组中每个元素的类型和约束
def test_04():
json_data = {
"data":[1,1,2,3,4,5],
"str":"hello"
}
json_schema = {
"type": "object",
"properties": {
"data":{
"type":"array",
"items":{"type":"integer"},
"minItems":1,
"maxItems":10,
#元素唯一
"uniqueItems":False,
#数组元素的类型
"items":{"type":"integer"}
},
"str":{"type":"string"}
},
"required": []
}
validate(instance=json_data,schema=json_schema)

(6)对象约束
| 关键字 | 作用 | 一句话说明 |
|---|---|---|
| properties | 定义对象里有哪些字段 | 写字段名 + 每个字段的类型 / 规则 |
| required | 定义哪些字段必填 | 少一个都报错 |
| additionalProperties | 是否允许出现未定义的字段 | 控制能不能乱加多余字段 |
| type | 声明这是一个对象 | 必须写 "type": "object" |
minProperties,maxProperties:指定对象的最小和最大属性数量
additionalProperties:控制是否允许对象中存在未在properties中定义的额外属性,默认为True(放在哪里就限制哪个层级)
def test_05():
json_data = {
"data":[1,1,2,3,4,5],
"str":"hello",
"add":1
}
json_schema = {
"type": "object",
"properties": {
"data":{
"type":"array",
"items":{"type":"integer"},
"minItems":1,
"maxItems":10,
#元素唯一
"uniqueItems":False,
#数组元素的类型
"items":{"type":"integer"}
},
"str":{"type":"string"}
},
# "additionalProperties": False,
"minProperties":1,
"maxProperties":3,
"required": []
}
validate(instance=json_data,schema=json_schema)

(7)必要属性
def test_06():
json_data = {
"data":[1,1,2,3,4,5],
# "str":"hello",
# "add":1
}
json_schema = {
"type": "object",
"properties": {
"data":{
"type":"array",
"items":{"type":"integer"}
},
"str":{"type":"string"}
},
"required": ["data","str"]
}
validate(instance=json_data,schema=json_schema)

(8)依赖关系
dependentRequired:定义属性之间的依赖关系,如果某个属性存在,必须返回另外一个属性(单向限制)
def test_07():
json_data = {
"username":"zhangsan",
"age":18,
# "height":175,
"gender":"female"
}
json_schema = {
"type": "object",
"properties": {
"username": {"type": "string"},
"age": {"type": "integer"},
"height": {"type": "integer"},
"gender": {"type": "string"}
},
"required": [],
#返回age必须返回height和gender
"dependentRequired": {
"age": ["height","gender"]
}
}
validate(instance=json_data,schema=json_schema)

6、logging日志
(1)介绍
级别从低到高:
debug → 调试细节
info → 正常流程
warning → 警告
error → 错误
critical → 严重错误

(2)使用
①自定义logger输出到控制台
import logging
#指定输出级别:Info及以上
logging.basicConfig(level=logging.INFO)
#自定义日志对象
logger=logging.getLogger("my_logger")
#配置自己的日志级别
logger.setLevel(level=logging.DEBUG)
logger.debug("调试信息")
logger.info("普通信息")
logger.warning("警告")
logger.error("报错信息")
logger.critical("严重错误")

②自定义logger输出到日志文件
常用格式占位符:
| 占位符 | 作用 |
|---|---|
%(asctime)s |
日志记录的时间,默认格式为 YYYY-MM-DD HH:MM:SS,毫秒 |
%(levelname)s |
日志的级别名称 |
%(name)s |
日志器的名称(getLogger 时传入的自定义名称) |
%(filename)s |
调用日志的代码所在的文件名 |
%(funcName)s |
调用日志的代码所在的函数名,主程序中为 <module> |
%(lineno)d |
调用日志的代码所在的行号(数字类型) |
%(message)s |
你手动传入的日志内容 |
import logging
#指定输出级别:Info及以上
logging.basicConfig(level=logging.INFO)
#自定义日志对象
logger=logging.getLogger("my_logger")
#配置自己的日志级别
logger.setLevel(level=logging.DEBUG)
#创建文件处理器--将日志输出到mylog.log(可以自动创建
handler=logging.FileHandler(filename="mylog.log")
#创建日志格式器对象
formatter = logging.Formatter(
"%(asctime)s %(levelname)s [%(name)s] [%(filename)s] (%(funcName)s:%(lineno)d) - %(message)s"
)
#将格式器设置到处理器上
handler.setFormatter(formatter)
#将处理器添加到日志记录器中
logger.addHandler(handler)
logger.debug("this is a debug message")
logger.info("this is a info message")
logger.warning("this is a warning message")
logger.error("this is a error message")
logger.critical("this is a critical message")


7、测试报告allure
(1)生成测试结果
pytest --alluredir=allure-results(会自动创建文件夹)

(2)查看测试报告
①在浏览器显示
allure serve [options] <allure-results>
allure serve .\allure-results\ 自动在浏览器打开测试报告,端口号随机
allure serve --port 8989 .\allure-results 指定端口号
(.\allure-results\ 和 .\allure-results 完全没有区别,效果一模一样!带\明确表示是文件夹,不带\系统会自动判定)
②从测试结果生成测试报告
allure generate [options] <allure-results> -o <reports>
<allure-results>测试结果文件夹 <reports>html测试报告文件夹(两者均可自动生成)
allure generate .\allure-results\ -o .\allure-reports\ --clean 生成的同时清除历史测试报告

(3)一些补充
若重新pytest,则会继续生成.json文件,若历史生成的.json文件不清空,可以在测试报告中查看历史执行情况。

pytest --alluredir=allure-results --clean-alluredir 清空历史测试情况
也可以在配置文件中指定

更多推荐
所有评论(0)