目录
一、pytest 单元测试框架
二、单元测试框架和自动化测试框架
2.1 什么是自动化测试框架
2.2 自动化测试框架的作用
2.3 单元测试框架和自动化测试框架的关系
三、pytest 简介
四、pytest 接口自动化框架搭建
4.1 pycharm工具安装
4.2 python安装与环境配置
4.3 pycharm 安装pytest
4.4 pycharm中导入requests库
五、基本语法
5.1 定义变量
5.2 变量类型
5.2.1 Numbers(数字)
5.2.2 String(字符串)
5.2.3 List(列表)
5.2.4 Tuple(元组)
5.2.5 Dictionary(字典)
5.3 运算符
5.3.1 算术运算符
5.3.2 比较运算符
5.3.3 赋值运算符
5.3.4 逻辑运算符编辑
5.3.5 成员运算符
5.3.6 身份运算符
5.4 条件语句
5.5 循环语句
5.6 日期和时间
5.7 print打印语句
5.9 main方法
5.10 函数
5.11 面向对象
5.11.1 面向对象的调用
5.11.2 init 函数
5.11.3 类的继承
5.11.4 类的重写
5.11.5 类属性与方法
5.11.6 单下划线、双下划线、头尾双下划线说明
六、接口测试
6.1 简单的Get 请求
6.2 简单的POST请求
七、pytest 用例开发常见问题
7.1 脚本读取配置文件
7.2 运行接口自动化用例
7.2.1 设置 pytest的测试环境
7.2.2 命名规则
7.2.3 接口入参
7.2.4 返参检验
7.2.5 运行结果
7.3 pytest参数化
7.4 文件读写
7.5 上传图片与视频
7.6 数据库参数化
单元测试是指在软件开发当中,针对软件的最小单位(函数、方法)进行正确性的检查测试
常见的单元测试框架:
java:junit 和 testng
python:unittest 和 pytest
单元测试框架主要做什么?
自动化测试框架是为了完成一个指定的系统的自动化测试,而封装的一整套的、完善的代码框架,主要是封装一些自动化的基础模块、自动化的管理模块、自动化的统计模块等组成一个自动化框架。
单元测试框架只是自动化测试框架中的组成部分之一,就是由这些一个个部分,组成了一个完整的自动化测试框架。
官方下载路径:Download PyCharm: Python IDE for Professional Developers by JetBrains
下载社区版即可满足日常的接口自动化脚本开发与维护
官方下载路径:Download Python | Python.org
配置python的环境变量,pycharm工具配置默认运行pytest框架和运行的python环境
设置默认的python运行环境:
在python中,变量不需要定义,直接使用:
def Test(abs1,abs2,abs3):
a = 10
b = 20
c =[1,20,40,[50,10,"where"]]
c[0] = abs1
c[1] = abs2
c[2] = abs3
print("abs1的值为:%d"%c[0])
print("abs2的值为:%d"%c[1])
print("abs3的值为:%d"%c[2])
Test(10,20,30)
数字数据类型用于存储数值,他们是不可改变的数据类型,这意味着改变数字数据类型会分配一个新的对象。
a = 10
b = 20
也可以使用del 语句删除一些对象的引用。
list1 = ['physics', 'chemistry', 1997, 2000]
list2 = [1, 2, 3, 4, 5,6,7,8 ]
print("list1[0]:",list1[0]) #输出为list1[0]: physics
print("list2[1:5]:",list2[1:5]) #输出为list2[1:5]: [2, 3, 4, 5]
del list1[2] #删除list[2]的对象的引用
print("list1:",list1) #输出为list1: ['physics', 'chemistry', 2000]
字符串或串(String)是由数字、字母、下划线组成的一串字符。
word = 'word'
sentence ="这是个句子"
paragraph = """这是一个段落
包含了多个语句"""
print(word)
print(sentence)
print(paragraph)
List(列表) 是 Python 中使用最频繁的数据类型。列表可以完成大多数集合类的数据结构实现。它支持字符,数字,字符串甚至可以包含列表(即嵌套)。列表用 [ ] 标识,是 python 最通用的复合数据类型。
列表中值的切割也可以用到变量 [头下标:尾下标] ,就可以截取相应的列表,从左到右索引默认 0 开始,从右到左索引默认 -1 开始,下标可以为空表示取到头或尾。
letters =['c','h','e','n','r','a','n','j','d','h','a','o']
a = letters[1:11:3]
print("letters:", a) #输出结果为letters: ['h', 'r', 'j', 'a']
letters ="chenranjdhao"
a = letters[1:11:3] #取下标为1,截止下标为11之间,每隔3个下标间距分别取值
print("letters:", a) #输出结果为letters: hrja
letters ="chenranjdhao"
a = letters[1:5] #取下标为1,截止下标小于5之间的值
print("letters:", a) #输出结果为letters: henr
list = [] #空列表
list.append('Google') #使用 append() 添加元素
list.append('Runoob')
print("更新列表数据:",list) #输出结果为 更新列表数据: ['Google', 'Runoob']
元组是另一个数据类型,类似于 List,用 () 进行标识,内部元素用逗号隔开。
元组不能二次赋值,相当于只读列表。
tuple = ('runoob', 786, 2.23, 'john', 70.2)
tinytuple = (123, 'john')
print(tuple) # 输出完整元组
print(tuple[0]) # 输出元组的第一个元素
print(tuple[1:3]) # 输出第二个至第四个(不包含)的元素
print(tuple[2:]) # 输出从第三个开始至列表末尾的所有元素
tup1 = (12, 34.56)
tup2 = ('abc', 'xyz')
tup3 = tup1 + tup2
print(tup3) # 输出结果为(12, 34.56, 'abc', 'xyz')
L = ('spam', 'Spam', 'SPAM!')
print(L[1:]) # 输出结果为('Spam', 'SPAM!')
print(len(L)) # 输出结果为3
字典(dictionary)是除列表以外python之中最灵活的内置数据结构类型。列表是有序的对象集合,字典是无序的对象集合。
两者之间的区别在于:字典当中的元素是通过键来存取的,而不是通过偏移存取。
字典用"{ }"标识。字典由索引(key)和它对应的值value组成。
dict ={}
dict['one'] ="this is one"
dict[2]="this is two"
tinydict = {'name': 'runoob','code':6734, 'dept': 'sales'}
print(dict['one']) #输出为:this is one
print(dict[2]) #输出为:this is two
print(tinydict) #输出为:{'name': 'runoob', 'code': 6734, 'dept': 'sales'}
print(tinydict.values()) #输出为:dict_items([('name', 'runoob'), ('code', 6734), ('dept', 'sales')])
print(tinydict.keys()) #输出为:dict_keys(['name', 'code', 'dept'])
print(tinydict.values()) #输出为:dict_values(['runoob', 6734, 'sales'])
tinydict = {'Name': 'Zara', 'Age': 7, 'Class': 'First'}
tinydict['Age'] = 8 # 更新
tinydict['School'] = "RUNOOB" # 添加
print("tinydict['School']: ", tinydict['School']) #输出结果为RUNOOB
print("tinydict['Age']: ", tinydict['Age']) #输出结果为8
del tinydict # 删除字典
print(tinydict) # 运行结果报错
tinydict.clear() # 清空字典所有条目
print(tinydict) # 运行结果为{}
var1 = 'Hello \nWorld!' #字符串换行
print("e" in var1) #输出结果为 true
print('ela' not in var1) #输出结果为 true
print(var1)
a = 20
b = 20
if (a is b):
print("1 - a 和 b 有相同的标识")
else:
print("1 - a 和 b 没有相同的标识")
if (a is not b):
print("2 - a 和 b 没有相同的标识")
else:
print("2 - a 和 b 有相同的标识")
var =103
if(var ==100): print("变量var的值为100")
elif(var ==101): print("值不为100")
else:print('锤子')
count = 0
while count < 5:
print (count, " is less than 5")
count = count + 1
else:
print(count, " is not less than 5")
for letter in 'chedganfgrhao':
print("当前字母:%s"% letter) #遍历字符串
fruits = ['banana','apple','mango']
for fruits in fruits:
print('当前水果: %s'% fruits) #遍历列表中的值
for num in range(10,20): # 迭代 10 到 20 之间的数字
for i in range(2,num): # 根据因子迭代
if num%i == 0: # 确定第一个因子
j=num/i # 计算第二个因子
print ('%d 等于 %d * %d' % (num,i,j))
break # 跳出当前循环
else: # 循环的 else 部分
print ('%d 是一个质数' % num)
for letter in 'Python':
if letter == 'h':
pass
print('这是 pass 块')
print('当前字母 :', letter)
Python 提供了一个 time 和 calendar 模块可以用于格式化日期和时间。
import calendar
import time
ticks =time.time()
print("当前时间戳为:",ticks) #当前时间戳为: 1691552283.167663
localtime = time.localtime(time.time())
print ("本地时间为 :", localtime) #本地时间为 : time.struct_time(tm_year=2023, tm_mon=8, tm_mday=9, tm_hour=11, tm_min=38, tm_sec=3, tm_wday=2, tm_yday=221, tm_isdst=0)
# 格式化成2022-10-17 11:45:39形式
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) #2023-08-09 11:38:03
# 格式化成Sat Mar 28 22:24:24 2022形式
print(time.strftime("%a %b %d %H:%M:%S %Y", time.localtime())) #Wed Aug 09 11:38:03 2023
# 将格式字符串转换为时间戳
a = "Mon Oct 28 22:24:24 2022"
print(time.mktime(time.strptime(a, "%a %b %d %H:%M:%S %Y"))) #1666967064.0
cal = calendar.month(2023, 8)
print("以下输出2023年8月份的日历:\n",cal)
# str + str:
print("我要打印的是字符串:"+ names[0])
# str + int 解决方案:
print("我要打印的是int: %d"%lists[0])
print("我要打印的是字符串: %s"%lists[0])
print("我要打印的是数组:{}".format(names[2][0]))
main 方法的作用只是用于调试,模拟其他模块调用本模块的场景以及自测,main语句在其他程序调用本模块时,不执行。
函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段。函数能提高应用的模块性,和代码的重复利用率。Python提供了许多内建函数,比如print()。但你也可以自己创建函数,这被叫做用户自定义函数。
# 函数定义的格式
def printme(str):
print(str)
return
printme("自定义函数001")
# 带参数的函数
def printinfo(name, age=35):
"打印任何传入的字符串"
print("Name: ", name)
print("Age ", age)
return
printinfo(age=50, name="Miki") # 输出结果为:Miki,50
printinfo(name="miki") # 输出结果为:Miki,35
# 不定长参数,用(*变量名)进行表示
def printinfo(arg1, *vartuple):
print("输出: \n ",arg1)
for var in vartuple:
print(var)
return
printinfo(10) # 输出结果为:10
printinfo(70, 60, 50) # 输出结果为:70 60 50
__init__()方法是一种特殊的方法,被称为类的构造函数或初始化方法,当创建了这个类的实例时就会调用该方法。类里面的方法都需要加上(self),申请这是类里面自己的函数,self 不是 python 关键字,我们把他换成 runoob 也是可以正常执行的。
类以外的函数,就不需要加self,申请该函数是类外的函数方法(不受init函数的影响)。在实际编程过程中,是使用类还是函数?如果功能存在公共的模块,就使用类;没有的话,就使用函数即可。
# 定义父类
class Parent:
def myMethod(self):
print( '调用父类方法')
# 定义子类
class Child(Parent):
def myMethod(self):
print('调用子类方法')
print(12)
c= Child() # 子类实例
c.myMethod() # 子类调用重写方法
class A: # 定义类 A
.....
class B: # 定义类 B
.....
class C(A, B): # 继承类 A 和 B
.....
class Parent: # 定义父类
def myMethod(self):
print '调用父类方法'
class Child(Parent): # 定义子类
def myMethod(self):
print '调用子类方法'
c = Child() # 子类实例
c.myMethod() # 子类调用重写方法
类的私有属性
__private_attrs:两个下划线开头,声明该属性为私有,不能在类的外部被使用或直接访问。在类内部的方法中使用时 self.__private_attrs。
类的方法
在类的内部,使用 def 关键字可以为类定义一个方法,与一般函数定义不同,类方法必须包含参数 self,且为第一个参数
类的私有方法
__private_method:两个下划线开头,声明该方法为私有方法,不能在类的外部调用。在类的内部调用 self.__private_methods
class JustCounter:
__secretCount = 0 # 私有变量
publicCount = 0 # 公开变量
def count(self):
self.__secretCount += 1
self.publicCount += 1
print self.__secretCount
counter = JustCounter()
counter.count()
counter.count()
print counter.publicCount
print counter.__secretCount # 报错,实例不能访问私有变量
_foo: 以单下划线开头的表示的是 protected 类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *
__foo: 双下划线的表示的是私有类型(private)的变量, 只能是允许这个类本身进行访问了。
__foo__: 定义的是特殊方法,一般是系统定义名字 ,类似 __init__() 之类的。
步骤:
取响应体的内容,响应体的Contest-Type存在两种方式“text/html” 和 “application/json”,通过res.text文本或res.json()方式接收接口响应的内容。
步骤:
常用函数:
cookie 问题:把登录的cookie保存下来,传到下一个接口去发请求,session发起登录,通过session传递发起其他接口请求
重定向问题:禁止重定向,请求参数中传入allow_redirects=False;解决请求重定向问题,请求同一个session即可避免重定向,如 s = requests.session();login.login(s).post_login();
以URL域名、登录账号与密码参数化为例:
将参数化文件整理到配置文件中(项目里新建file文件),以xxx.ini、xxx.cfg 或 xxx.txt格式,再编写读取配置文件类,通过函数进行配置文件的调用
from configparser import ConfigParser # 读取配置文件模块
import os # 读取配置文件的本地路径
class ReadFile():
def __init__(self):
# strict=False,主要解决配置文件configparser读取重复配置项的的问题
self.conf = ConfigParser(strict=False)
# 不引用os系统内置方法,第三方模块调用时,因取不到配置文件的本地路径而报错
filepath = os.path.dirname(os.path.abspath(__file__)) + '/config.cfg'
# print(filepath)
# filepath = 'E:/work/GoodsApi/config/config.cfg'
self.conf.read(filepath,encoding="utf-8")
# 获取session字符串的URL
def getSessionHost(self):
session_host = self.conf.get('host','session_host')
return session_host
# 获取token字符串的URL
def getTokenHost(self):
token_host = self.conf.get('host','token_host')
return token_host
# 获取XX系统域名地址
def getOaperate_host(self):
oaperate_host = self.conf.get('host','oaperate_host')
return oaperate_host
# 获取用户名和密码
def getLogin(self):
username = self.conf.get('login', 'username')
password = self.conf.get('login', 'password')
return {"username":username,"password":password}
#通过main方式调试代码
if __name__ == '__main__':
session_host = ReadFile().getSessionHost()
token_host = ReadFile().getTokenHost()
oaperate_host = ReadFile().getOaperate_host()
# print(session_host)
# print(token_host)
print(oaperate_host)
# username = ReadFile().getLogin()
# print(username['username'])
# print(username['password'])
以上读取配置类代码中,主要注意两个问题:
# 获取当前文件的路径 os.path.abspath(__file__)
# 获取当前文件的完整路径 os.path.dirname(os.path.abspath(__file__))
# 根据当前文件的完整路径,字符串拼接出来的路径 os.path.dirname(os.path.abspath(__file__)) + '\config.cfg'
# filepath = os.path.dirname(os.path.abspath(__file__)) + '\config.cfg'
# 获取文件的绝对路径
path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
# 通过决定路径,拼接字符串的路径
filepath = os.path.join(path,"config","config.cfg")
# print("filepath:",filepath)
self.conf.read(filepath,encoding="utf-8")
config配置项已经存在了,因系统读取到2个以上,会报错。如果不希望报错,直接采用新的配置项覆盖,那么需要在configparser.ConfigParser(strict=False)
方法中strict
参数设置未False
# 实例化ConfigParser方法时,需要在ConfigParser()方法中strict参数设置未False
self.conf = ConfigParser(strict=False)
配置文件,读取配置文件,最后一步就是引用读取配置文件的函数,更便捷的维护及使用参数化的变量。
import requests
from config import readFile # 引入读取配置文件的类文件
class LoginTest():
def __init__(self):
self.s = requests.session()
self.session_host = readFile.ReadFile().getSessionHost() #实例化并赋值
self.token_host = readFile.ReadFile().getTokenHost()
self.getLogin = readFile.ReadFile().getLogin()
self.getOaperate_host = readFile.ReadFile().getOaperate_host()
# print(self.session_host)
# print(self.token_host)
def post_session(self,username,password):
session_url = self.session_host + "/saas-sso/login" # 直接使用参数化的URL
# print("session_url:",session_url)
data = {
"username": self.getLogin['username'],
"password": self.getLogin['password'],
"verification": "",
"loginFromSystemCode": "TAGGG_TKHH",
"encryptType": 1
}
res = self.s.post(url = session_url, data = data)
# print(res.json())
session = res.cookies
return session
def get_code(self):
session = LoginTest().post_session(username=self.getLogin['username'],password=self.getLogin['password'])
code_url = self.session_host + "/oauth2/authorize?client_id=TTTRG_TK&responses_type=code&redirectts_uri=" + self.getOperate_host + "/"
res = self.s.get(cookies = session,url = code_url,allow_redirects=False)
code = res.json()["data"]["code"]
# print("code:",code)
return code
if __name__ == '__main__':
# getLogin = readFile.ReadFile().getLogin()
# LoginTest().post_session(getLogin['username'],getLogin['password'])
# LoginTest().get_code()
总结步骤:
用例文件名及用例中的函数或模块,以test开头或test结尾
import pytest
from common.LoginTest import LoginTest
from api.Label.Lable import lable
from api.Label.LableTypes import lableTypes
# 用例1:登录
def test_Login():
LoginTest().post_token()
# 用例2:添加标签类型
def test_addLableTypes():
lableTypes().add_lableTypes("自动化测试新增标签类型A")
# 用例3:添加标签
def test_addLable():
lable().add_labe("自动化测试新增标签A")
if __name__ == '__main__':
pytest(['-s','testLogin.py'])
单个变量参数化(全量测试):
@pytest.mark.parametrize("username",("18688276555","","1868827"))
@pytest.mark.parametrize("username",("18688276555","","1868827"))
# 对 username 进行全量测试
def test_session(username):
res =LoginTest().post_session(username,password = readFile.ReadFile().getLogin()['password'])
print(res)
assert res.json()["code"] == "200"
组合交叉参数化:
多个变量参数化(入参、预期结果参数化):
# 用例:对入参username、password、出参code,进行参数化
@pytest.mark.parametrize("username,password,code",
[("18688276555","A","200"),
("","B","100"),
("1868827","C","100"),
])
def test_session2(username,password,code):
res =LoginTest().post_session(username = username,password = password)
assert code == res['code']
config目录下,新建desc.txt 文件,在公共目录下读取、写入、追加 desc.txt 文件
import os
path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
file =os.path.join(path,"config","desc.txt")
print(file)
# 读取desc.txt 文件('r' 也可以写成'rb',即二进制文件读取,则可不申明utf-8)
with open(file,'r',encoding = 'utf-8')as f:
text = f.readline(7)
text2 = f.read()
print(text)
print(text2)
# 完全覆盖重写desc.txt 文件('w' 也可以写成'wb',即二进制文件写入,则可不申明utf-8)
with open(file,'w',encoding = 'utf-8')as f:
f.write("abcdefg")
print(text)
print(text2)
# 追加写入desc.txt 文件
with open(file,'a',encoding = 'utf-8')as f:
f.write("abcdefg")
print(text)
print(text2)
def upload(self):
path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
file = os.path.join(path, "config", "123.jpg")
login.Login(self).post_login()
url = "https://xxxx.com"
filename = file
file = {"uploadFiles":open(filename,"rb")}
res = self.post(url,files = file)
print(res.txt)
# 通过二进制流的方式进行文件的读取
open(file,"rb")
[mysql]
host = rm.mysql.aliyuncs.com
port = 13308
user = qa
password = Oc0Nqir76yBWxo12311ba
# 数据库配置文件,多种类型存储数据
[dbinfo]
dbinfo ={
"host":"rm.mysql.aliyuncs.com",
"user":"qa",
"password":"Oc0Nqir76yBWxo12311ba",
"port":13308
}
封装数据库连接,封装SQL语句运行处理:
class ReadFile():
def __init__(self):
self.conf = ConfigParser(strict=False)
path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
filepath = os.path.join(path,"config","config.cfg")
self.conf.read(filepath,encoding="utf-8")
# 封装读取config配置文件中的数据
def getValue(self,section='hostname',option = 'host'):
value = self.conf.get(section,option)
return value
# 获取MYSQL数据库
def getMysql(self):
host = self.getValue('mysql','host')
username = self.getValue('mysql','username')
password = self.getValue('mysql','password')
port = self.getValue('mysql','port')
return {"host":host,"username":username,"password":password,"port":(int)port}
# 获取MYSQL数据库(读配置二)
def getMysql1(self):
dbinfo = self.getValue('dbinfo','dbinfo')
return dbinfo
class Connect():
def __init__(self,database = 'tx_product'):
dbinfo = ReadFile().getMysql()
# 使用配置二的函数方法
# dbinfo = eval(ReadFile().getMysql1())
# print(type(dbinfo))
self.con = pymysql.Connect(database=database,
cursorclass=pymysql.cursors.DictCursor,
**dbinfo)
self.curos = self.con.cursor()
# 封装SQL查询
def select_sql(self,sql):
self.curos.execute(sql)
# res = self.curos.fetchall() # 获取所有的值
res = self.curos.fetchone() # 获取第一行的值
# res = self.curos.fetchmany(size) # 自定义返回数量
print(res)
self.con.close() # 关闭SQL连接函数
return res
# 封装SQL更新
def update_sql(self,sql):
try:
self.curos.execute(sql)
self.con.commit() # 执行更新语句,并提交
except:
self.con.rollback() # 执行更新语句报错,则回滚
self.con.close()
if __name__ == '__main__':
con =Connect()
s_sql = "select product_id from product"
res = con.select_sql(s_sql)
print(res[0]['product_id']) # list列表取某组数据中具体值)