接口测试的测试点
功能测试:单接口功能、业务场景功能
性能测试:响应时长、错误率、吞吐量、服务器资源使用率
安全测试:敏感数据是否加密、SQL注入、其他
本篇文章以接口功能测试为主。
接口用例设计方法:
单接口测试用例设计:
正向用例设计:
(1)必选参数。所有必选参数给正确数据
(2)组合参数:所有必选+任意可选,给正确数据
(3)全部参数:所有必选+所有可选,给正确参数
反向用例设计:
(1)功能异常:数据格式正确,不能履行接口功能
(2)数据异常:数据格式不正确(空格、特殊字符、字母、长度----等价类,边界值)
(3)参数异常:(多参、少参、无参、错误参数)
业务场景接口测试
(1)尽量模拟用户实际使用场景
(2)尽量用最少的用例,覆盖最多的接口请求
(3)一般情况下,覆盖正向的测试即可
案例1:登录接口用例设计(以下用例只是部分)
案例2:添加用户用例设计(以下用例只是部分)
接口测试工具-postman
关于postman的详细操作见:
接口测试工具之postman juejin.cn/post/695424…
使用requests库实现接口测试
Requests库 是 Python编写的,基于urllib 的 HTTP库,使用方便。
安装:pip install requests
或者指定国内镜像源安装:pip install requests -i https://pypi.douban.com/simple/
设置http请求语法
resp = requests.请求方法(url='URL地址', params={k:v}, headers={k:v},data={k:v}, json={k:v}, cookies='cookie数据'(如:令牌))
入门案例:访问百度接口
我们在练习过程中,可以使用开源商城tpshop来进行练习,具体安装在百度上搜索即可
案例:开源商城tpshop的登录接口
import requests
# 发送 post 请求,指定url、请求头、请求体,并获取响应结果
resp = requests.post(url="http://127.0.0.1/index.php?m=Home&c=User&a=do_login&t=0.04841131938292054",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"username": "13012345678", "password": "1234567", "verify_code": "8888"})
# 打印响应结果 - 文本
print(resp.text)
# 打印响应结果 - json
print(resp.json())
案例:商城调用登录接口,增加cookie参数(tpshop项目:cookie+Session认证。)
import requests
resp_v = requests.get(url="http://127.0.0.1/index.php?m=Home&c=User&a=verify&r=0.47350402574416184")
my_cookie = resp_v.cookies
resp = requests.post(url="http://127.0.0.1/index.php?m=Home&c=User&a=do_login&t=0.04841131938292054",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"username": "18800000000", "password": "1234567", "verify_code": "8888"},
cookies=my_cookie)
print(resp.json())
以下方式是使用session认证,不需要获取cookie--掌握
import requests
# 1. 创建一个 Session 实例。
session = requests.session()
# 2. 使用 Session 实例,调 get方法,发送获取验证码请求。
session.get(url="http://127.0.0.1/index.php?m=Home&c=User&a=verify&r=0.47350402574416184")
# 3. 使用 同一个 Session 实例,调用 post方法,发送登录请求。
resp = session.post(url="http://127.0.0.1/index.php?m=Home&c=User&a=do_login&t=0.04841131938292054",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"username": "18892089999", "password": "123456", "verify_code": "8888"})
print(resp.json())
面试题 Cookie 和 Session 区别
- 数据存储位置:
cookie存储在浏览器;session存储在服务器。
- 安全性:
cookie中的数据可以随意获取,没有安全性可言。Session的数据多为加密存储,安全较高!
- 数据类型:
cookie支持的数据类型受浏览器限制,较少;Session直接使用服务器存储,支持所有数据类型
- 大小:
cookie大小默认 4k; Session 大小约为服务器存储空间大小
Unittest框架集成Requests库
Unittest的相关详细知识见下面文档:
Python框架之UnitTest: juejin.cn/post/712533…
案例:登录接口,单个测试方法
import unittest
import requests
class TestLogin(unittest.TestCase):
def test01_login(self):
resp = requests.post(url="http://127.0.0.1/index.php?m=Home&c=User&a=do_login&t=0.04841131938292054",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"username": "18892081111", "password": "123456", "verify_code": "8888"})
print(resp.json())
# 断言状态码
self.assertEqual(200, resp.status_code)
# 断言 success的值为True
self.assertEqual(True, resp.json().get("success"))
PyMySQL操作数据库
应用场景:
校验测试数据:接口发送请求之后,明确会对数据库中的某个字段进行修改,但是响应结果中不体现
构造测试数据:比如测试数据使用一次就失败(用手机号添加员工);测试前,无法保证测试数据一定存在(比如:列表查询)
安装PyMySQL:pip install PyMySQL
或者pip install PyMySQL -i https://pypi.douban.com/simple/
操作数据库的基本流程:
1、导包;import pymysql
2、创建数据库连接(connection);conn = pymysql.connect()
3、获取游标对象(cursor);cursor = conn.cursor()
4、执行操作;cursor.execute('sql语句')
(4.1)查询语句:不会对数据库产生影响,有结果集返回,使用fetch提取返回数据;
(4.2)增删改语句:会对数据库产生影响。成功:提交事务:conn.commit()
;失败:回滚事务:conn.rollback()
5、关闭游标对象(cursor);cursor.close()
6、关闭数据库连接(connection);conn.close()
案例:
# 1、导包;`
import pymysql
# 2、创建数据库连接(connection);`conn = pymysql.connect()
conn = pymysql.connect(host="localhost",port=3306,user="root",password="root",database="mysql",charset="utf8")
# 3、获取游标对象(cursor);`cursor = conn.cursor()
cursor = conn.cursor()
# 4、执行操作;`cursor.execute('sql语句')`
cursor.execute('select version()')
# 提取一行结果
res = cursor.fetchone()
print(res) # 返回元组
print(res[0]) #取返回的第一个值
# 5、关闭游标对象(cursor);`cursor.close()`
cursor.close()
# 6、关闭数据库连接(connection);`conn.close()`
conn.close()
常用方法说明:
fetchone():从结果集中提取一行
fetchmany(size):从结果集中提取size行
fetchall():提取所有结果集
属性rownumber:可以设置游标位置(cursor.rownumber = 0(设置游标归零))
数据库工具类封装:
(1)获取、关闭连接
import pymysql
# 封装数据库工具类
class DBUtil(object):
# 添加类属性
conn = None
@classmethod
def __get_conn(cls):
# 判断 conn 是否为空, 如果是,再创建
if cls.conn is None:
cls.conn = pymysql.connect(host="localhost",port=3306,user="root",password="root",database="mysql",charset="utf8")
# 返回非空连接
return cls.conn
@classmethod
def __close_conn(cls):
# 判断conn 不为空,需要关闭。
if cls.conn is not None:
cls.conn.close()
cls.conn = None
(2)查询一条记录
# 常用方法:查询一条结果
@classmethod
def select_one(cls, sql):
cursor = None
res = None
try:
# 获取连接
cls.conn = cls.__get_conn()
# 获取游标
cursor = cls.conn.cursor()
# 执行 查询语句
cursor.execute(sql)
# 提取一条结果
res = cursor.fetchone()
except Exception as err:
print("查询sql错误:", str(err))
finally:
# 关闭游标
cursor.close()
# 关闭连接
cls.__close_conn()
# 将查询sql执行的 结果,返回
return res
if __name__ == '__main__':
res = DBUtil.select_one("select * from person;")
print("查询结果为:", res)
(3)增删改数据时方法封装
# 常用方法:增删改数据
@classmethod
def uid_db(cls, sql):
cursor = None
try:
# 获取连接
cls.conn = cls.__get_conn()
# 获取游标
cursor = cls.conn.cursor()
# 执行 uid 语句
cursor.execute(sql)
print("影响的行数:", cls.conn.affected_rows())
# 提交事务
cls.conn.commit()
except Exception as err:
# 回滚事务
cls.conn.rollback()
print("增删改 SQL 执行失败:", str(err))
finally:
# 关闭游标
cursor.close()
# 关闭连接
cls.__close_conn()
if __name__ == '__main__':
DBUtil.uid_db("update person set age = 30 where id = 2;")
接口对象封装
核心思想:代码分层思想,分为接口对象层和测试脚本层
(1)接口对象层:对接口进行封装,封装好之后,给测试用例层调用,面向对象类封装实现。
(2)测试用例层:调用接口对象层封装的方法,获取响应结果,断言进行接口测试,借助unittest框架实现。
封装思想:
(1)将动态变化的数据设计到方法的参数;
(2)将固定不变的,直接写成方法的实现;
(3)将响应结果通过返回值传出;
我们以TPshop开源商城为例,在没有封装之前的代码为:
import unittest
import requests
class TestTpshopLogin(unittest.TestCase):
# 测试登录成功
def test01_login_ok(self):
# 创建 session 实例
session = requests.Session()
# 使⽤实例,调⽤get 发送获取验证码请求
session.get(url="http://127.0.0.1/index.php?m=Home&c=User&a=verify&r=0.47350402574416184")
# 使⽤实例,调⽤post 发送登录请求
resp = session.post(url="http://127.0.0.1/index.php?m=Home&c=User&a=do_login&t=0.04841131938292054",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"username": "18892089999", "password": "123456", "verify_code": "8888"})
print("响应结果 =", resp.json())
# 断⾔:
self.assertEqual(200, resp.status_code)
self.assertEqual(1, resp.json().get("status"))
self.assertEqual("登陆成功", resp.json().get("msg"))
# 测试⼿机号不存在
def test02_tel_not_exists(self):
# 创建 session 实例
session = requests.Session()
# 使⽤实例,调⽤get 发送获取验证码请求
session.get(url="http://127.0.0.1/index.php?m=Home&c=User&a=verify&r=0.47350402574416184")
# 使⽤实例,调⽤post 发送登录请求
resp = session.post(url="http://127.0.0.1/index.php?m=Home&c=User&a=do_login&t=0.04841131938292054",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"username": "18892099999", "password": "123456", "verify_code": "8888"})
print("响应结果 =", resp.json())
# 断⾔:
self.assertEqual(200, resp.status_code)
self.assertEqual(-1, resp.json().get("status"))
self.assertEqual("账号不存在!", resp.json().get("msg"))
# 测试密码错误
def test03_pwd_err(self):
# 创建 session 实例
session = requests.Session()
# 使⽤实例,调⽤get 发送获取验证码请求
session.get(url="http://127.0.0.1/index.php?m=Home&c=User&a=verify&r=0.47350402574416184")
# 使⽤实例,调⽤post 发送登录请求
resp = session.post(url="http://127.0.0.1/index.php?m=Home&c=User&a=do_login&t=0.04841131938292054",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"username": "18892089999", "password": "999999", "verify_code": "8888"})
print("响应结果 =", resp.json())
# 断⾔:
self.assertEqual(200, resp.status_code)
self.assertEqual(-2, resp.json().get("status"))
self.assertEqual("密码错误!", resp.json().get("msg"))
对以上代码进行封装:
封装接口对象层:
class TpshopLoginApi(object):
# 发送验证码请求
@classmethod
def get_verify(cls, session):
session.get(url="http://127.0.0.1/index.php?m=Home&c=User&a=verify&r=0.47350402574416184")
# 发送登录请求
@classmethod
def login(cls, session, login_data):
resp = session.post(
url="http://127.0.0.1/index.php?m=Home&c=User&a=do_login&t=0.04841131938292054",
data=login_data)
return resp
断言方法封装:
def common_assert(self, resp, status_code, status, msg):
# 断⾔:
self.assertEqual(status_code, resp.status_code)
self.assertEqual(status, resp.json().get("status"))
self.assertEqual(msg, resp.json().get("msg"))
测试用例层封装结果:
import unittest
import requests
from TPshop_login_api import TpshopLoginApi
from tp_shop_assert import common_assert
class TestShopLogin(unittest.TestCase):
# 添加类属性
session = None
@classmethod
def setUpClass(cls) -> None:
cls.session = requests.Session()
def setUp(self) -> None:
TpshopLoginApi.get_verify(self.session)
# 测试登录成功
def test01_login_ok(self):
login_data = {"username": "18892089999", "password": "123456", "verify_code": "8888"}
resp = TpshopLoginApi.login(self.session, login_data)
print("登录成功的结果:", resp.json())
common_assert(self, resp, 200, 1, "登陆成功")
# 测试⼿机号不存在
def test02_tel_not_exists(self):
login_data = {"username": "18892099999", "password": "123456", "verify_code": "8888"}
resp = TpshopLoginApi.login(self.session, login_data)
print("登录成功的结果:", resp.json())
# 断⾔:
common_assert(self, resp, 200, -1, "账号不存在!")
# 测试密码错误
def test03_pwd_err(self):
login_data = {"username": "18892089999", "password": "999999", "verify_code": "8888"}
resp = TpshopLoginApi.login(self.session, login_data)
print("登录成功的结果:", resp.json())
# 断⾔:
common_assert(self, resp, 200, -2, "密码错误!")
以上代码参数化:
# 参数化的数据,实际项目中需要单独保存
json_data = [
{
"req_body": {"username": "18892080505", "password": "123456", "verify_code": "8888"},
"status_code": 200,
"status": 1,
"msg": "登陆成功"
},
{
"req_body": {"username": "18892099999", "password": "123456", "verify_code": "8888"},
"status_code": 200,
"status": -1,
"msg": "账号不存在!"
},
{
"req_body": {"username": "18892080505", "password": "999999", "verify_code": "8888"},
"status_code": 200,
"status": -2,
"msg": "密码错误!"
}
]
# 封装函数,将 数据 转换为 元组列表。
def read_json_data():
# 将[{},{},{},{}]格式转化成[(),(),(),()]
list_data = []
for item in json_data:
tmp = tuple(item.values())
list_data.append(tmp)
print(list_data)
return list_data
在通用方法中实现参数化:
class TestShopLogin(unittest.TestCase):
# 添加类属性
session = None
@classmethod
def setUpClass(cls) -> None:
cls.session = requests.Session()
def setUp(self) -> None:
TpshopLoginApi.get_verify(self.session)
# 测试登录
@parameterized.expand(read_json_data())
def test_tpshop_login(self, req_body, status_code, status, msg):
resp = TpshopLoginApi.login(self.session, req_body)
print("登录成功的结果:", resp.json())
common_assert(self, resp, status_code, status, msg)
接口自动化测试框架思想
目录结构
5个目录、2个文件:
api/: 存储接口对象层(自己封装的 接口)
scripts/: 存储测试脚本层 (unittest框架实现的 测试类、测试方法)
data/: 存储 .json 数据文件
report/: 存储 生成的 html 测试报告
common/: 存储 通用的 工具方法
confifig.py: 存储项目的配置信息(全局变量)
run_suite.py: 组装测试用例、生成测试报告的 代码
日志
日志的级别
logging.DEBUG:调试级别【高】
logging.INFO:信息级别【次高】
logging.WARNING:警告级别【中】
logging.ERROR:错误级别【低】
logging.CRITICAL:严重错误级别【极低】
特性:日志级别设定后,只有比该级别低的日志会写入日志。
日志代码,无需手写实现。会修改、调用即可!
import logging.handlers
import logging
import time
def init_log_config(filename, when='midnight', interval=1, backup_count=3):
# 1. 创建日志器对象
logger = logging.getLogger()
# 2. 设置日志打印级别
logger.setLevel(logging.DEBUG)
# logging.DEBUG 调试级别
# logging.INFO 信息级别
# logging.WARNING 警告级别
# logging.ERROR 错误级别
# logging.CRITICAL 严重错误级别
# 3.1 创建 输出到控制台 处理器对象
st = logging.StreamHandler()
# 3.2 创建 输出到日志文件 处理器对象
# when 字符串,指定日志切分间隔时间的单位。midnight:凌晨:12点。
# interval 是间隔时间单位的个数,指等待多少个 when 后继续进行日志记录
# backupCount 是保留日志文件的个数
fh = logging.handlers.TimedRotatingFileHandler(filename, when=when, interval=interval, backupCount=backup_count, encoding='utf-8')
# 4. 创建日志信息格式
fmt = "%(asctime)s %(levelname)s [%(filename)s(%(funcName)s:%(lineno)d)] - %(message)s"
formatter = logging.Formatter(fmt)
# 5.1 日志信息格式 设置给 控制台处理器
st.setFormatter(formatter)
# 5.2 日志信息格式 设置给 日志文件处理器
fh.setFormatter(formatter)
# 6.1 给日志器对象 添加 控制台处理器
logger.addHandler(st)
# 6.2 给日志器对象 添加 日志文件处理器
logger.addHandler(fh)
if __name__ == '__main__':
# 初始化日志
init_log_config('tp_shop_log.log', interval=3, backup_count=5)
# 打印输出 日志信息
logging.warning("这是警告日志的信息")
logging.debug("这是debug日志信息")
a = 9999
logging.error(f"这是error级别的信息a = {a}")
全量字段的校验-对断言的补充
流程:
(1)定义json语法校验格式
(2)⽐对接口实际响应数据是否符合json校验格式
全量字段校验需要安装jsonschema
pip install jsonschema -i https://pypi.douban.com/simple/
在线工具校验:www.jsonschemavalidator.net
python代码入门:
# 1. 导包
import jsonschema
# 2. 创建校验规则
schema = {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"code": {
"type": "integer"
},
"message": {
"type": "string"
}
},
"required": ["success","code","message"]
}
# 准备待校验的数据
data = {
"success": True,
"code": 200,
"message": "操作成功"
}
# 3. 调用 validate 方法,实现校验
result = jsonschema.validate(instance = data,schema=schema)
print("result = ", result)
# None: 代表校验通过
# ValidationError:数据与校验规则不符
# SchemaError: 校验规则语法有误
JSON Schema语法中常用的关键字(来源某视频课程):
(1)type关键字:约束数据类型
integer —— 整数
string —— 字符串
object —— 对象
array —— 数组 --> python:list 列表
number —— 整数/⼩数
null —— 空值 --> python:None
boolean —— 布尔值
语法:
{
"type": "数据类型"
}
(2)properties关键字:是 type关键字的辅助。用于 type 的值为 object 的场景。指定对象中 每个字段的校验规则。 可以嵌套使用。
语法:
{
"type": "object",
"properties":{
"字段名1":{规则},
"字段名2":{规则},
......
}
}
(3)required关键字:校验对象中必须存在的字段。字段名必须是字符串,且唯⼀
语法:
{
"required": ["字段名1", "字段名2", ...]
}
(4)const关键字:校验字段值是⼀个固定值。
语法:
{
"字段名":{"const": 具体值}
}
(5)pattern关键字:指定正则表达式,对字符串进行模糊匹配
语法:
{
"字段名":{"pattern": "正则表达式"}
}
补充中......
行动吧,在路上总比一直观望的要好,未来的你肯定会感 谢现在拼搏的自己!如果想学习提升找不到资料,没人答疑解惑时,请及时加入扣群: 320231853,里面有各种软件测试+开发资料和技术可以一起交流学习哦。
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!