python requests+pytest+allure接口自动化测试框架

背景

本地写了登录,查询商品goods,查询form表单几个简单的接口,没有什么复杂的校验,单纯的是为了测试这套自动化框架

/login:登录接口(post,content-type:application/json)

/query/goods:登录后查询接口(post,content-type:application/json,为了测试接口关联的场景)

/form:查询form表单(post,content-type:application/x-www-x-www-form-urlencoded)

该框架主要用到的第三方库:

requests(用于进行发送接口请求)

pymysql(操作mysql数据库)

redis(操作redis数据库)

faker(生成数据)

yaml(操作yaml文件)

logging(用于日志的输出打印)

configparser(获取配置文件)

pytest(数据驱动和前后置操作)

allure(生成测试报告)

框架的主要构成:

configs:配置文件

common:常用的工具类

log:日志文件

test_case:测试用例

test_data:测试数据

conftest.py:存放 fixture 的配置文件

pytest.ini:pytest的配置文件

run.py:程序入口

1.configs:配置文件

配置文件里面可以放一些固定的信息,比如mysql数据库和redis的链接信息,测试环境地址等

config.ini配置文件是以key=value键值对的方式存在,所有的键值对都是以节section为单位结合在一起的,用[]表示节。

[database]
host = 127.0.0.1
port = 3306
user = root
password = 12345678
database = mysql

[url]
test = http://127.0.0.1:5000/

以上述代码的格式将配置写入config.ini配置文件中,[database]和[url]就是section节。

2.common:常用的工具类

common是一个文件夹,里面放置很多常用的工具类:

1.db_operate.py(数据库的操作)

该文件主要是使用pymysql对数据库增删改查等操作的封装,可以用于前置数据的准备或者数据库数据校验,以及后置的数据清理。

# 导入pymysql
import logging
import pymysql
from test_api.common.get_config import GetConfig


class DbOperation:
	def __init__(self):
		# 链接数据库,查询的结果以字典的形式返回
		self.connect = pymysql.connect(**GetConfig().get_database(), cursorclass=pymysql.cursors.DictCursor)
		# 创建游标
		self.cursor = self.connect.cursor()

	# 查询一条符合条件的数据
	def query_data(self, sql):
		try:
			self.cursor.execute(sql)
			result = self.cursor.fetchone()
			return result
		except pymysql.Error as error:
			logging.error(error)

	# 执行sql(包含插入,删除,修改)
	def exe_data(self, sql):
		try:
			self.cursor.execute(sql)
			self.connect.commit()
		except pymysql.Error as error:
			logging.error(error)

	# 关闭链接和数据库
	def __del__(self):
		self.cursor.close()
		self.connect.close()
2.generate_data.py(生成随机数据)

有的时候我们测试需要模拟不同用户的请求操作,需要用到动态的参数。该文件主要是通过第三方库faker生成数据,比如随机数,手机号码,身份证号码,姓名等。

from faker import Faker


class GeneratedData:
    def __init__(self, faker_lang='zh_CN'):
        self.faker_data = Faker(faker_lang)
    
    # 生成随机姓名
    def generatedName(self):
        return self.faker_data.name()
    
    # 生成随机手机号码
    def generatedPhone(self):
        return self.faker_data.phone_number()

    # 生成随机的身份证号码
    def generatedIdentity(self):
        return self.faker_data.ssn()

    # 生成随机的银行卡号
    def generatedCard(self):
        return self.faker_data.credit_card_number()

    # 生成随机数
    def generatedRandomNumber(self, n):
        return str(self.faker_data.random_number(n))
3.get_config.py(获取配置文件)

设置完配置文件后,我们需要方法去获取配置文件的值,configparser用于获取config.ini的值。

获取配置的方法,主要两种:

get方法:获取指定section下指定键的值

items方法:获取指定section下所有键的值

import configparser
import os

# 需要读取配置文件的路径
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "configs/config.ini")


class GetConfig:
	def __init__(self):
		# 创建对象
		self.cf = configparser.ConfigParser()
		# 读取该配置文件
		self.cf.read(config_path)

	# 获取数据库配置信息
	def get_database(self):
		database_dict = {}
		# f.items(section="database")获取节点database下的配置信息
		for key, value in self.cf.items(section="database"):
			if key == "port":
				database_dict[key] = int(value)
			else:
				database_dict[key] = value
		return database_dict
	
	# 获取测试环境地址
	def get_url(self, name):
		return self.cf.get("url", name)
4.get_yaml.py(获取yaml文件内容)

使用yaml库来获取yaml文件里的内容,也可以说是用来获取测试用例。该文件里封装了读取,写入和清空。写入和清空是为了接口关联而存在的。

写了两种读取用例的方法:

第一种读取主流程用例,默认每个yaml文件里第一个用例为主流程用例;

第二种是读取单个接口的所有用例,针对单个接口所有场景的测试;

注:读取方法中**kwargs不定长参数,是为了接受动态参数存在的。如果你传了动态参数,该方法会使用Template替换。

Template方法是替换yaml文件中的占位符$,使用随机参数替换。yaml里的占位符名称要和调用该方法传的键相同。

import yaml
import os
from string import Template
from test_api.common.log import Log


class GetYaml:
	def __init__(self, yaml_name):
		yaml.warnings({'YAMLLoadWarning': False})
		self.yaml_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), f"test_data/{yaml_name}")
		self.getYamlLog = Log()

	# 读取主流程用例
	def read_main_yaml(self, link=False, **kwargs):
		with open(self.yaml_path, mode="r", encoding='utf-8') as content:
			load_content = yaml.safe_load(content)
			# 接口关联标志,如果为true则直接返回yaml文件内容
			if link:
				self.getYamlLog.info_log(f"=======接口关联,获取all.yaml文件的关联参数{load_content}=======")
				return load_content
			else:
				# 如果kwargs为true,替换成yaml文件里的占位符$
				if kwargs:
					self.getYamlLog.info_log(f"===========替换动态参数===========")
					self.getYamlLog.info_log(f"替换参数为{kwargs}")
					return yaml.safe_load(Template(str(load_content[0])).safe_substitute(kwargs))
				else:
					return load_content[0]

	# 读取单条测试用例各场景
	def read_yaml(self, testcase_name, **kwargs):
		with open(self.yaml_path, mode="r", encoding='utf-8') as content:
			load_content = yaml.safe_load(content)
			for num in range(len(load_content)):
				# 判断测试用例名称是否在yaml文件里
				if testcase_name in load_content[num].keys():
					if kwargs:
						return yaml.safe_load(Template(str(load_content[num][testcase_name])).safe_substitute(kwargs))
					else:
						return load_content[num][testcase_name]
			else:
				return "该测试用例不存在"

	# 写入
	def write_yaml(self, detail):
		with open(self.yaml_path, mode="a", encoding='utf-8') as content:
			yaml.dump(detail, content, default_flow_style=False)

	# 清空
	def clear_yaml(self):
		with open(self.yaml_path, mode="w", encoding='utf-8') as content:
			content.truncate()
5.log.py(日志文件)

该py文件主要是封装了文件日志和控制台日志的输出打印。通过不同的颜色来区分日志的不同级别。

import logging
import os
import time
import colorlog

log_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "log")
localTime = time.strftime("%Y%m%d")


class Log:
	def __init__(self):
		self.logger = logging.getLogger("日志输出")
		self.logger.setLevel(logging.INFO)
		# 设置日志格式和颜色
		self.log_format = colorlog.ColoredFormatter('%(log_color)s[%(asctime)s]-%(levelname)s:%(message)s', log_colors={
			'DEBUG': 'cyan',
			'INFO': 'green',
			'WARNING': 'yellow',
			'ERROR': 'red',
			'CRITICAL': 'red,bg_white',
		})
		# 判断是否有handles,如果有则不会再次添加,防止造成重复打印日志的情况。如果没有则去添加
		if not self.logger.handlers:
			# 设置文件日志的路径名称
			self.fileHandle = logging.FileHandler(filename=log_path + f'/{localTime}.log', mode='a', encoding='UTF-8')
			# 设置文件日志的格式
			self.fileHandle.setFormatter(self.log_format)
			# 将文件日志添加处理器
			self.logger.addHandler(self.fileHandle)
			###############################################
			# 设置控制台日志
			self.streamHandle = logging.StreamHandler()
			# 设置控制台日志的格式
			self.streamHandle.setFormatter(self.log_format)
			# 将控制台日志添加处理器
			self.logger.addHandler(self.streamHandle)

	def info_log(self, message):
		self.logger.info(message)

	def debug_log(self, message):
		self.logger.debug(message)

	def warning_log(self, message):
		self.logger.warning(message)

	def error_log(self, message):
		self.logger.error(message)
6.request_operate.py(统一接口请求)

封装了统一的接口请求和请求要素的日志打印,目前为止只封装了get和post方法。

import requests
from test_api.common.get_config import GetConfig
from test_api.common.log import Log

path = GetConfig().get_url("test")


class RequestUtil:
	session = requests.session()
	requestLog = Log()

	def all_request(self, url, methods, headers=None, params=None, request_data=None):
		if methods.upper() == "GET":
			try:
				response = self.session.get(url, headers, params)
				self.request_log(url, methods, headers, params, response.json())
				return response
			except Exception as e:
				self.requestLog.error_log(f"请求失败,{e}")
		elif methods.upper() == "POST":
			try:
				if headers['Content-Type'] == "application/json":
					response = self.session.post(url=url, json=request_data)
				else:
					response = self.session.post(url=url, data=request_data)
				self.request_log(url, methods, headers, request_data, response.json())
				return response
			except Exception as e:
				self.requestLog.error_log(f"请求失败,{e}")

	def request_log(self, url, method, headers, data, response_json):
		self.requestLog.info_log(f"请求接口地址==============>{url}")
		self.requestLog.info_log(f"请求接口的请求方式=============>{method}")
		self.requestLog.info_log(f"请求接口的请求头==============>{headers}")
		self.requestLog.info_log(f"请求接口的请求参数==============>{data}")
		self.requestLog.info_log(f"请求接口的响应==============>{response_json}")
7.assert_data.py(断言封装)

根据传入的预期返回结果和实际返回结果,对双方的key和value进行对比断言。并且打印出结果

from test_api.common.log import Log


class Check_data:
	def __init__(self):
		self.check_data_log = Log()
		self.check_list = []

	# 检查接口json格式的返回key,是否与预期结果返回key相等
	def key_check(self, really_key, except_key):
		if len(really_key) != len(except_key):
			self.check_list.append(False)
			Log().error_log("========预期结果和实际结果字段数量不一致========")
		else:
			for key in except_key.keys():
				if key in really_key:
					self.check_list.append(True)
				else:
					self.check_list.append(False)
					Log().error_log(f"=======预期结果中不存在实际结果中的{key}字段========")

		if False in self.check_list:
			Log().error_log(f"断言方式[json-key]======>预期结果{really_key},实际结果{except_key},断言失败")
		else:
			Log().info_log(f"断言方式[json-key]======>预期结果{really_key},实际结果{except_key},断言通过")

	def value_check(self, really_value, except_value):
		# except_value_dict = eval(str(except_value))
		for key, value in really_value.items():
			if value in except_value.values():
				self.check_list.append(True)
			else:
				self.check_list.append(False)
				Log().error_log(f"=======实际结果中{key}字段的{value}值和预期结果不一致========")

		if False in self.check_list:
			Log().error_log(f"断言方式[json-value]======>预期结果{really_value},实际结果{except_value},断言失败")
		else:
			Log().info_log(f"断言方式[json-value]======>预期结果{really_value},实际结果{except_value},断言通过")

3.test_data:测试数据

我是用yaml文件来管理测试用例,每个yaml文件里存放一个接口的全部测试场景。

yaml 是一种数据序列化语言,yaml 文件使用 .yml 或 .yaml 扩展名,并遵循特定的语法规则

语法特点:

 大小写敏感;

使用缩进表示层级关系;

缩进时不允许使用Tab键,只允许使用空格;

缩进的空格数目不重要,只要相同层级的元素左侧对齐即可。

也可以使用yaml转换器来进行json和yaml进行转化:Online YAML Tools - TOOLFK

以下是一个简单的登录接口,成功和失败场景的测试用例。包括:用例编号,接口地址,请求方式,请求数据,接口返回。

- test_login_01:
   url: login
   method: POST
   headers:
     Content-Type: application/json
   data:
     username: "$username"
     password: "$password"
   valid:
     msg: "用户$username登录成功"
     uid: "123456"
     username: "$username"

- test_login_02:
   url: login
   method: POST
   headers:
     Content-Type: application/json
   data:
     username: ""
     password: "$password"
   valid: 账号或密码错误

4.conftest.py: 存放 fixture 的文件

conftest是pytest中主要存放fixture的配置文件,它可以进行前置操作,数据的共享。

比如获取登录接口token的方法,前置操作数据库数据的方法,清空yaml文件的方法等等都可以写在conftest文件里。

import pytest
from test_api.common.getYaml import GetYaml


# 请空all.yaml文件,以便之后写入参数,保持接口关联性
@pytest.fixture(scope="class")
def clear_yamlFile():
	GetYaml("all.yaml").clear_yaml()

注:clear_yamlFile这个方法是清空yaml文件的,主要是接口关联的场景需要。后一个接口的参数要用到前一个接口的返回,将需要的参数放到专门的yaml文件里,第一个接口请求之前先清空。

5.test_case:测试用例

我是写了一个从登录,登录后查询goods,还有个content-type为form表单的post接口

通过pytets.mark.parametrize()进行参数化,将yaml里的测试用例信息传给testcase_list。

from string import Template
import allure
import pytest
from test_api.common.request_operate import RequestUtil
from test_api.common.get_yaml import GetYaml
from test_api.common.get_config import GetConfig
from test_api.common.generate_data import GeneratedData
from test_api.common.assert_data import Check_data
from test_api.common.crypt_data import MD5_encode

path = GetConfig().get_url("test")
getYamlLogin = GetYaml("login.yaml")
getYamlGoods = GetYaml("goods.yaml")
getYamlForm = GetYaml("form.yaml")
getYamlAll = GetYaml("all.yaml")
request = RequestUtil()
check = Check_data()
# md5加密测试接口
# username = MD5_encode("admin")
# password = MD5_encode("admin123")
# 随机生成账号密码
username = GeneratedData().generatedName()
password = GeneratedData().generatedRandomNumber(8)


@allure.feature("登录查询form表单")
class Test_main:

	@allure.story("登录")
	# 将该方法打上login标签
	@pytest.mark.login
	# 失败重新执行2次
	@pytest.mark.flaky(reruns=2)
	# 执行顺序(第一个执行)
	@pytest.mark.run(order=1)
	# 参数化,请求参数,方法,地址从login.yaml文件里取
	@pytest.mark.parametrize("testcase_list", [getYamlLogin.read_main_yaml(username=username, password=password)])
	def test_login(self, clear_yamlFile, testcase_list):
		testcase = testcase_list["test_login_01"]
		url = path + testcase["url"]
		response = request.all_request(url, testcase["method"], testcase['headers'], request_data=testcase["data"])
		# 多重断言assume,前面断言失败后面也会执行
		pytest.assume(response.status_code == 200)
		check.key_check(response.json(), testcase["valid"])
		check.value_check(response.json(), testcase["valid"])
		# 将该接口返回写入到all.yaml文件中
		GetYaml("all.yaml").write_yaml({"uid": response.json()["uid"]})

	@allure.story("查询goods")
	@pytest.mark.run(order=2)
	@pytest.mark.queryGoods
	@pytest.mark.parametrize("testcase", [getYamlGoods.read_main_yaml()])
	def test_queryGoods(self, testcase):
		url = path + testcase["url"]
		# 获取上一个接口的返回参数用于该接口的请求
		data = Template(str(testcase["data"])).safe_substitute(getYamlAll.read_main_yaml(link=True))
		response = request.all_request(url, testcase["method"], testcase['headers'], request_data=eval(data))
		pytest.assume(response.status_code == 200)
		check.key_check(response.json(), testcase["valid"])
		check.value_check(response.json(), testcase["valid"])

	@allure.story("查询form表单")
	@pytest.mark.run(order=3)
	@pytest.mark.formData
	@pytest.mark.parametrize("testcase", [getYamlForm.read_main_yaml(name=username)])
	def test_formData(self, testcase):
		url = path + testcase["url"]
		request_data = testcase["data"]
		response = request.all_request(url, testcase["method"], testcase['headers'], request_data=request_data)
		pytest.assume(response.status_code == 200)
		check.key_check(response.json(), testcase["valid"])
		check.value_check(response.json(), testcase["valid"])

6.pytest.ini(pytest的配置文件)

该配置文件可以设置pytest运行的具体文件或者方法;默认命令行;设置标签(运行指定的测试用例)

[pytest]
# 指定运行的路径
testpaths = ./test_case
# 指定运行的文件
# python_files = test_login.py
python_files = test_main.py
;指定运行的类和方法
;python_class
;python_function
# 默认命令行
addopts = -vs
# 标签
markers =
    login
    queryGoods
    formData
    para

7.run.py(运行并且生成测试报告):

import pytest
import os

path = os.path.dirname(__file__)
# f"{path}/test_case/test_login.py::Test_login"
if __name__ == "__main__":
	pytest.main(['--alluredir=./report/xml'])

pytest.main()里面传list对象,list里有多个命令行参数。在list里面写入你要执行类或者函数的路径

'--alluredir=./report/xml'在当前项目目录下生成测试报告。

在terminal当前项目下输入 allure serve ./report/xml生成测试报告python requests+pytest+allure接口自动化测试框架_第1张图片

python requests+pytest+allure接口自动化测试框架_第2张图片

你可能感兴趣的:(pytest,自动化)