2.接口自动化测试

接口自动化测试

基于pytest和allure构建接口自动化测试框架构与项目。

框架目录结构

我们要构建一个自动化测试框架,就要以项目的概念来对项目中所有的代码文件进行划分目录和文件结构,不同的代码功能不一样,所以我们需要设计一个合理的目录结构,以方便与测试开发团队的其他人员进行测试功能的开发与测试,也方便将来的项目代码维护。

根目录/
 ├─ config.py    # 项目代码配置文件
 ├─ pytest.ini    # pytest模块配置文件
 ├─ main.py      # 主程序,执行入口
 ├─ api/         # 封装被测试项目的api接口存放目录[用于mock测试、冒烟测试]
 ├─ data/        # 测试数据/测试用例的存放目录
 ├─ results/     # 测试报告结果生成目录
 ├─ reports/     # HTML测试报告生成目录
 ├─ tests/       # 测试用例脚本存放目录
 ├─ libs/         # 第三方工具类的存放目录[开源模块,不是当前项目封装的模块]
 └─ utils/        # 自定义工具类的存放目录[当前项目自己封装的模块]

配置文件,config.py,代码:

import pathlib  # 路径操作模块,替代 os.path模块,os.path采用字符串来操作路径,pathlib采用面向对象来操作路径

# 项目目录的主目录路径[字符串路徑]
BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix()  # 基本操作系统转换路径的分隔符 as_posix
# 項目目录的主目录路径[路径对象]
BASE_DIR = pathlib.Path(BASE_DIR_STR)

# 项目名
WEB_NAME = "路飞自动化接口测试框架"

# 测试自动化项目的运行端口与IP地址
HOST = "127.0.0.1"
PORT = 8088

入口文件,main.py,代码:

import pytest
import os
import sys

import config

if __name__ == '__main__':
    os.system(f"rd /s /q results")
    os.system(f"rd /s /q reports")

    # 让python解释器,追加3个项目中的核心目录为导包路径
    sys.path.insert(0, str(config.BASE_DIR / "api"))
    sys.path.insert(0, str(config.BASE_DIR / "tests"))
    sys.path.insert(0, str(config.BASE_DIR / "utils"))

    # 启动pytest框架
    pytest.main()

    # 生成报告html文件
    os.system('allure generate ./results -o ./reports')

    # 基于http协议打开HTML测试报告
    os.system(f'allure open ./reports -h {config.HOST} -p {config.PORT}')

在根目录下创建pytest配置文件pytest.ini,内容如下:

[pytest]
addopts =-s -v --alluredir=./results
testpaths = ./
python_files = test_*.py
python_classes = Test*
python_functions = test_*

OK,完成了上面操作以后,我们就可以写一个测试用例来测试下现在我们的基本框架是否能正常运行了。

tests/users/test_login.py,代码:

import allure
import config


@allure.epic(config.WEB_NAME)
@allure.feature("用户模块")
@allure.story("登录")
class TestLogin(object):
    @allure.severity(allure.severity_level.CRITICAL)
    def test_username_by_empty(self,):
        allure.dynamic.title("用户名为空,登陆失败")
        allure.dynamic.description("测试用户名为空的登陆结果")
        allure.attach('附件内容', '附件名字')

    def test_password_by_empty(self,):
        allure.dynamic.title("密码为空,登陆失败")
        allure.dynamic.description("测试密码为空的登陆结果")
        allure.attach('附件内容', '附件名字')

运行框架,效果:

 

当然自动化接口框架的构建开发过程肯定不是一夜之间就能完成的,所以我们需要长期构建,不断完善的准备。所以需要使用git代码版本管理工具把代码推送到git仓库中进行代码版本的管理(在企业中一般我们会使用公司内部构建的gitlab平台来管理内部项目,但现在我们处于学习阶段,所以可以先把代码提交到gitee码云上)。

代码托管

注意:公司内部的代码不要私自自己往gitee(码云),github去推。

gitee官网地址:https://gitee.com/

准备工作

需要提前在当前开发机子上安装git代码版本管理工具。

windows下载地址:https://git-scm.com/

创建仓库

 

我们是学习,所以我这创建项目库名luffytest。项目库名建议是英文的。

选择git-flow自定义分支模型。

所谓的分支,其实就是一个项目的代码的不同流程版本。

git-flow分支命名规范:

分支前缀描述
master 生产环境分支,将来可以部署到生产环境(公司的外网服务器)的代码
release 预发布环境分支,将来可以部署到预发布环境的代码,也可以是rel
develop 开发环境分支,也可以是dev
feature 新功能,新增业务分支,也可以是feat
hotfix 修复bug问题、漏洞分支,也可以是fix
test 测试相关分支

补充说明:

release/feature/xxx   # 表示待发布分支下的xxx业务功能相关代码
develop/feature/xxx   # 表示开发分支下的xxx业务的新功能代码
develop/test/xxx      # 表示开发分支下的正在测试的xxx业务功能的代码
develop/hotfix/xxx    # 表示开发分支下的修复xxx业务相关功能的bug

# 当然,分支名称并不是固定的,只要能让开发人员一目了然,名字都可以换,所以也可以这样:
release/xxx   # 表示待发布的xxx业务新功能代码
feature/xxx   # 表示正在开发的xxx业务功能代码
test/xxx      # 表示正在测试的xxx业务功能代码
hotfix/user   # 表示正在修复bug的xxx业务功能的代码

创建私有空仓库以后的界面:

免密提交代码

接下来,我们可以使用ssh连接远程的git仓库,需要先在本地电脑下生成ssh秘钥对。

# 例如我的码云账号是 649641514@qq.com,那么该账号生成ssh秘钥对(基于rsa加密算法生成的秘钥对,公钥和私钥)
ssh-keygen -t rsa -C "649641514@qq.com"

复制终端下出现的公钥信息,复制到码云上面。

# 查看生成的秘钥对中的公钥(私钥id_rsa保存在自己电脑不要乱动,公钥id_rsa.pub需要复制文件内容到码云平台)
cat C:\Users\Administrator/.ssh/id_rsa.pub
# 把公钥进行复制到码云平台上 https://gitee.com/profile/sshkeys

在浏览器打开https://gitee.com/profile/sshkeys,保存公钥。

切换项目的仓库地址,设置线上仓库

git config --global user.name "mooluo"
git config --global user.email "649641514@qq.com"

# 在项目根目录下初始化git仓库
cd luffytest/  # 具体的路径根据自己的设置而定
git init

# git remote remove origin # 删除仓库地址,origin可以理解是一个变量,因为当前时一个新仓库,所以不需要执行这段。
git remote add origin git@gitee.com:mooluo_admin/luffytest.git # 新仓库地址,等同于origin= git..../luffycity.git

分支管理,git提交代码版本并同步到远程服务器。

git branch    # 查看分支
# 刚执行git init 初始化时,会没有分支,因此我们需要进行第一次的代码提交。

git add .
git commit -m "feature: 项目初始化"
# 经过上面的命令操作,本地的git就会自动生成一个master分支

# git branch <分支名称>    # 新建分支
# git branch test         # 例如:创建一个test分支

# git checkout <分支名称>  # 切换分支
# git checkout test       # 例如:切换到test分支,检出分支代码

# git branch -d <分支名称>   # 删除分支
# git branch -d test

# git push <远程仓库别名> --delete <分支名称>    # 删除远程服务器分支
# git push origin --delete test  # 例如:删除远程仓库origin中的test

# 推送代码记录到远程服务器的代码仓库
git push origin master # 推送的过程中,如果本地有该分支,但是线上没有这个分支,则git会自动在远程中创建该分支,默认的空仓库是一个分支都没有的。

使用.gitignore可以在git上传或下载代码时,把一些不必要记录的垃圾文件/目录过滤掉。

注意:必须保证.git目录 和.gitignore在同一级目录下,才能生效。

.gitignore编写参考地址:https://github.com/github/gitignore

.gitignore文件内容:

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# vue https://github.com/vuejs/vue/blob/dev/.gitignore
.DS_Store
node_modules
*.log
explorations
TODOs.md
dist/*.gz
dist/*.map
dist/vue.common.min.js
test/e2e/reports
test/e2e/screenshots
coverage
RELEASE_NOTE*.md
dist/*.js
packages/vue-server-renderer/basic.js
packages/vue-server-renderer/build.js
packages/vue-server-renderer/server-plugin.js
packages/vue-server-renderer/client-plugin.js
packages/vue-template-compiler/build.js
.vscode

# luffytest
reports/*
results/*
logs/*

记录并保存.gitignore到git中

git add .
git commit -m "feature: 新建.gitignore忽略文件"
# 推送代码记录到远程服务器的代码仓库
git push origin master

git commit 提交版本的描述信息,编写前缀规范:

描述前缀描述
feature: 本次提交的代码用于开发新功能,新增业务
fix: 本次提交的代码用于修复bug问题、漏洞
docs: 本次提交的代码用于修改文档,注释等相关
style: 本次提交的代码用于修改代码格式,不影响代码逻辑,常见的代码规范:PEP8,PEP484
refactor: 本次提交的代码用于项目/代码重构,理论上不影响现有功能(针对代码的重写,改造原来的模块/类/函数/方法)
perf: 本次提交的代码用于提升性能,代码优化
test: 本次提交的代码用于增加或修改测试用例代码
deps: 本次提交的代码用于升级项目依赖(更新依赖模块)

最终,成功提交了代码版本到gitee平台。

新增日志功能

针对的项目开发,将来肯定需要把测试框架这个项目保存公司的服务器的,所以如果项目在公司服务器报错了,我们有可能不在场,或者其他同时去运作,那么我们针对当前这个项目在这个运行期间,有没有出现异常,那就需要记录整个项目的运行信息。

config.py,新增如下日志配置代码,代码:

import pathlib  # 路径操作模块,替代 os.path模块,os.path采用字符串来操作路径,pathlib采用面向对象来操作路径

# 项目目录的主目录路径[字符串路徑]
BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix()  # 基本操作系统转换路径的分隔符 as_posix
# 項目目录的主目录路径[路径对象]
BASE_DIR = pathlib.Path(BASE_DIR_STR)

# 项目名
WEB_NAME = "路飞自动化接口测试框架-master"

# 测试自动化项目的运行端口与IP地址
HOST = "127.0.0.1"
PORT = 8088

"""日志配置"""
LOGGING = {
    "name": "luffytest",  # 日志处理器的名称,一般使用当前项目名作为名称
    "filename": (BASE_DIR / "logs/luffytest.log").as_posix(),  # 日志文件存储路径,注意,一定要在项目根目录下手动创建logs目录
    "charset": "utf-8",  # 日志内容的编码格式
    "backup_count": 31,  # 日志文件的备份数量
    "when": "d",   # 日志文件的创建间隔事件,m 表示每分钟创建1个,h表示每小时创建1个,d表示每天创建1个,m0~m6表示每周星期日~星期六创建1个,midnight表示每日凌晨
}

在项目根目录下创建logs目录,并编写日志工具类,utils/logger.py,代码:

import logging
import logging.handlers
import config


class LogHandle(object):
    """日志处理工具类"""

    def __init__(self, name=None, filename=None):
        self.name = config.LOGGING.get("name", "pytest")
        if name:
            self.name = name

        self.filename = config.LOGGING.get("filename", None)
        if filename:
            self.filename = filename

        self.charset = config.LOGGING.get("charset", "utf-8")
        self.log_backup_count = config.LOGGING.get("backup_count", 31)
        self.when = config.LOGGING.get("when", "d")

    def get_logger(self):
        # 创建logger,如果参数name表示日志器对象名,name为空则返回root logger
        logger = logging.getLogger(self.name)
        # 务必设置一个初始化的日志等级
        logger.setLevel(logging.DEBUG)
        # 这里进行判断,如果logger.handlers列表为空则添加,否则多次调用log日志函数会重复添加
        if not logger.handlers:
            # 创建handler
            fh = logging.handlers.TimedRotatingFileHandler(
                filename=self.filename,
                when=self.when,
                backupCount=self.log_backup_count,
                encoding=self.charset
            )
            sh = logging.StreamHandler()

            # 单独设置logger日志等级
            fh.setLevel(logging.INFO)
            # 设置输出日志格式
            simple_formatter = logging.Formatter(
                fmt="{levelname} {name} {module}:{lineno} {message}",
                style="{"
            )
            verbose_formatter = logging.Formatter(
                fmt="{levelname} {asctime} {name} {pathname}:{lineno} {message}",
                datefmt="%Y/%m/%d %H:%M:%S",
                style="{"
            )

            # 为handler指定输出格式
            fh.setFormatter(verbose_formatter)
            sh.setFormatter(simple_formatter)
            # 为logger添加的日志处理器
            logger.addHandler(fh)
            logger.addHandler(sh)

        return logger  # 直接返回logger

if __name__ == '__main__':
    from datetime import datetime

    log = LogHandle()
    log.filename = "../logs/pytest.log"
    logger = log.get_logger()
    logger.debug("日期测试")
    logger.info("普通日志")
    logger.warning("警告日志")
    logger.error("错误日志")
    logger.fatal("致命错误信息")

在测试用例中简单使用,tests/users/test_login.py,代码:

import allure
from logger import LogHandle


@allure.epic(config.WEB_NAME)
@allure.feature("用户模块")
@allure.story("登录")
class TestLogin(object):
    def test_username_by_empty(self,):
        logger = LogHandle().get_logger()
        logger.debug("debug")
        logger.info("运行日志-info")
        logger.warning("warning")
        logger.error("error")
        logger.critical("critical")

        allure.attach('附件内容', '附件名字')

    def test_login(self,):
        allure.attach('附件内容', '附件名字')

提交代码版本,代码:

git add .
git commit -m "feature: 新增日志功能"
# 推送代码记录到远程服务器的代码仓库
git push origin master

实现接口自定义测试

封装请求工具

接口测试肯定离不开http网络请求,在工作中python开发常用的http网络请求模块有:urllib,requests、httpx与aiohttp。

其中我们作为测试开发,比较常用的就是requests模块了。

Requests 是一个⽤Python编写,基于urllib的开源HTTP网络请求工具第三方库。它⽐python内置的urllib模块使用更加简单⽅便,可以节约我们⼤量的⼯作,完全满⾜HTTP测试需求。

requests快速使用
安装
pip install requests
GET请求

发送无参数的GET请求

demo/demo_requests.py,代码:

import requests

response = requests.get('http://httpbin.org/get')
print(response.content)  # 获取原生内容
print(response.text)   # 获取文本内容

发送有参数的GET请求

import requests

params = {
    'name': 'moluo',
    'age': 18
}
response = requests.get("http://httpbin.org/get?sex=1", params=params)
print(response.text)

接收数据

接收文本格式数据

import requests
 
params = {
 'name': 'moluo',
 'age': 18
}
response = requests.get("http://httpbin.org/get", params=params)
print(response.text)

接受json格式数据

import requests
import json

response = requests.get("http://httpbin.org/get")
# 方式I:
print(json.loads(response.text))
# 方式2:
print(response.json())

接受二进制格式数据

import requests

response = requests.get("http://httpbin.org/image/png")
with open("1.png", "wb") as f:
    f.write(response.content)

POST请求

发送表单数据

import requests

body = {'name': 'moluo', 'age': '22'}
response = requests.post("http://httpbin.org/post", data=body)
print(response.text)

发送json数据

import requests

data = {'name': 'moluo', 'age': '22'}
response = requests.post("http://httpbin.org/post", json=data)
print(response.text)

文件上传

import requests

# 支持上传一张或多张图片
files = {'avatar': open('1.png', 'rb')}
response = requests.post("http://httpbin.org/post", files=files)
print(response.text)
发送请求头
import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
}

response = requests.get("https://www.zhihu.com/explore", headers=headers)
print(response.text)

代理IP
import requests
# https://www.beesproxy.com/free
proxies = {
    # "http": "http://112.6.117.135:8085",
    "http": "http://223.68.190.136:9091",
}
req = requests.get('http://icanhazip.com/', proxies=proxies)
print(req.text)

会话保持

提取Cookie

import requests

r = requests.get('https://www.baidu.com')
print(r.cookies)
print(r.cookies['BDORZ'])
print(tuple(r.cookies))

发送Cookie

import requests

url = 'http://httpbin.org/cookies'
cookies = {'testCookies_1': 'Hello_Python3', 'testCookies_2': 'Hello_Requests'}
# 在Cookie Version 0中规定空格、方括号、圆括号、等于号、逗号、双引号、斜杠、问号、@,冒号,分号等特殊符号都不能作为Cookie的内容。
r = requests.get(url, cookies=cookies)
print(r.json())

其他方法

# 现在大部分的公司在开发项目都会会考虑多终端的问题,所以往往项目整体架构都是基于前后端分离的,那么实现前后端的分离的项目,往往服务端就是一个单独基于RestFul API接口的服务端。

import requests
# 只有args查询字符串参数,没有请求体
requests.get('http://httpbin.org/get')
requests.delete('http://httpbin.org/delete')
# 有请求体的
requests.post('http://httpbin.org/post')
requests.put('http://httpbin.org/put')
requests.patch('http://httpbin.org/patch')

自动化测试时,大家肯定都是希望自己写的代码越简洁越好,避免代码重复造成维护成本的提升,因此针对工作中常用的http网络请求模块requests,我们就需要在使用前对其进行简单的封装处理了。

utils/requestor.py,代码:

import requests
from logger import LogHandle

class Request(object):
    """http请求工具类"""
    def __init__(self):
        # 实例化session管理器,维持会话, 跨请求的时候保存参数
        self.session = requests.session()
        self.logger = LogHandle().get_logger()

    def send(self, method, url, params=None, data=None, json=None, headers=None, **kwargs):
        """
        :param method: http请求方式
        :param url: 请求地址
        :param params: 字典或bytes,作为参数增加到url中
        :param data: data类型传参,字典、字节序列或文件对象,作为Request的内容
        :param json: json传参,作为Request的内容
        :param headers: 请求头,字典
        :param kwargs: 若还有其他的参数,使用可变参数字典形式进行传递
        :return:
        """
        # 对异常进行捕获
        try:
            self.logger.info(f"请求方法:{method}")
            self.logger.info(f"请求地址:{url}")
            self.logger.info(f"请求头:{headers}")
            if params: self.logger.info(f"查询参数:params={params}")
            if data: self.logger.info(f"请求体:data={data}")
            if json: self.logger.info(f"请求体:json={json}")
            if kwargs: self.logger.info(f"额外参数:kwargs={kwargs}")
            response = self.session.request(method, url, params=params, data=data, json=json, headers=headers, **kwargs)
            self.logger.info(f"状态码:{response.status_code}")
            self.logger.info(f"响应头:{response.headers}")
            self.logger.info(f"响应体[纯文本]:{response.text}")
            self.logger.info(f"响应体[二进制]:{response.content}")
            self.logger.info(f"响应体[json]:{response.json()}")
            # 返回响应结果
            return response

        except Exception as e:
            # 异常处理 报错在日志中打印具体信息
            self.logger.error(f"请求失败:{e}")

    def __call__(self, method, url, params=None, data=None, json=None, headers=None, **kwargs):
        """当把一个对象,当成函数来使用,那么就指定执行当前对象的__call__"""
        return self.send(method, url, params=params, data=data, json=json, headers=headers, **kwargs)

if __name__ == '__main__':
    """"基本使用"""
    # 实例化
    request = Request()

    """发送get请求"""
    # # response = request("GET", 'http://httpbin.org/get')
    # response = request(method="GET", url='http://httpbin.org/get')
    # # 打印响应结果
    # print(response.text)


    """发送post请求"""
    # 请求地址
    url = 'http://httpbin.org/post'
    # 请求参数
    json = {"usernam": "moluo", "password": "123456"}
    # 请求头
    headers = {"company": "luffytest"}
    response = request(method="POST", url=url, json=json, headers=headers)
    # 打印响应结果
    print(response.text)

提交代码版本,代码:

git add .
git commit -m "feature: 新增requtsts http网络请求工具类"
# 推送代码记录到远程服务器的代码仓库
git push origin master

基于flask实现mockserver

在实际项目开发中,经常出现服务端与客户端分离的情况,那么我们在做测试开发时,有可能服务端的功能还没有实现,此时我们可以自己先模拟出服务器返回结果,直接对接口基本功能进行联调测试并编写自动化测试脚本,等服务器上线之后,切换server地址,直接可以简化调试时间,缩短项目测试周期。flask是一个轻量级的python web框架,非常适用于在测试开发中构建模拟api服务器。

安装模块
pip install flask
pip install pymysql
pip install flask_sqlalchemy
项目初始化

api/__init__.py,代码:

import config
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# SQLAlchemy初始化
db = SQLAlchemy()

app = Flask(__name__)

def init_app():
    # 加载配置
    app.config.from_object(config)

    # 加载mysql数据库配置
    db.init_app(app)

    # db创建数据表
    with app.app_context():
        db.create_all()

    return app

config.py,添加配置信息,代码:

import pathlib  # 路径操作模块,替代 os.path模块,os.path采用字符串来操作路径,pathlib采用面向对象来操作路径

# 项目目录的主目录路径[字符串路徑]
BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix()  # 基本操作系统转换路径的分隔符 as_posix
# 項目目录的主目录路径[路径对象]
BASE_DIR = pathlib.Path(BASE_DIR_STR)

# 项目名
WEB_NAME = "路飞自动化接口测试框架-master"

# 测试自动化项目的运行端口与IP地址
HOST = "127.0.0.1"
PORT = 8088

"""日志配置"""
LOGGING = {
    "name": "luffytest",  # 日志处理器的名称,一般使用当前项目名作为名称
    "filename": (BASE_DIR / "logs/luffytest.log").as_posix(),  # 日志文件存储路径,注意,一定要在项目根目录下手动创建logs目录
    "charset": "utf-8",  # 日志内容的编码格式
    "backup_count": 31,  # 日志文件的备份数量
    "when": "d",   # 日志文件的创建间隔事件,m 表示每分钟创建1个,h表示每小时创建1个,d表示每天创建1个,m0~m6表示每周星期日~星期六创建1个,midnight表示每日凌晨
}


"""mock server 的服务端配置"""
# 数据库连接
SQLALCHEMY_DATABASE_URI: str = "mysql+pymysql://root:123@127.0.0.1:3306/pytest?charset=utf8mb4"
# 查询时会显示原始SQL语句
SQLALCHEMY_ECHO: bool = True

# 调试模式
DEBUG = True
# 监听端口
API_PORT = 8000
# 监听地址
API_HOST = "0.0.0.0"

mock接口实现

api/models.py,代码:

from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash

from . import db


class BaseModel(db.Model):
    """公共模型"""
    __abstract__ = True  # 抽象模型
    id = db.Column(db.Integer, primary_key=True, comment="主键ID")
    name = db.Column(db.String(255), default="", comment="名称/标题")
    is_deleted = db.Column(db.Boolean, default=False, comment="逻辑删除")
    orders = db.Column(db.Integer, default=0, comment="排序")
    status = db.Column(db.Boolean, default=True, comment="状态(是否显示,是否激活)")
    created_time = db.Column(db.DateTime, default=datetime.now, comment="创建时间")
    updated_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")

    def __repr__(self):
        return f"<{self.__class__.__name__}: {self.name}>"


class User(BaseModel):
    """用户基本信息表"""
    __tablename__ = "py_user"
    name = db.Column(db.String(255), index=True, comment="用户账户")
    nickname = db.Column(db.String(255), comment="用户昵称")
    _password = db.Column(db.String(255), comment="登录密码")
    intro = db.Column(db.String(500), default="", comment="个性签名")
    avatar = db.Column(db.String(255), default="", comment="头像url地址")
    sex = db.Column(db.SmallInteger, default=0, comment="性别")  # 0表示未设置,保密, 1表示男,2表示女
    email = db.Column(db.String(32), index=True, default="", nullable=False, comment="邮箱地址")
    mobile = db.Column(db.String(32), index=True, nullable=False, comment="手机号码")

    # 存取器
    @property
    def password(self):  # user.password
        return self._password

    @password.setter
    def password(self, rawpwd):  # user.password = '123456'
        """密码加密"""
        self._password = generate_password_hash(rawpwd)

    def check_password(self, rawpwd):
        """验证密码"""
        return check_password_hash(self.password, rawpwd)

api/views.py,代码:

from flask import request
from sqlalchemy import or_

from . import app
from .models import User,db


@app.route("/user/register", methods=["POST"])
def register():
    """
    用户信息注册
    :return:
    """
    try:
        data = request.json
        # 创建用户数据
        user = User(**data)
        db.session.add(user)
        db.session.commit()
        return {"msg": "注册成功!", "data": {"id":user.id, "name": user.name}}, 200
    except Exception as e:
        return {"msg": "注册失败!", "data": {}}, 400


@app.route("/user/login", methods=["POST"])
def login():
    """
    用户登录
    :return:
    """
    user = User.query.filter(
        or_(
            User.mobile == request.json.get("username"),
            User.name == request.json.get("username"),
            User.email == request.json.get("username")
        )
    ).first()  # 实例化模型

    if not user:
        return {"msg": "登录失败!用户不存在!", "data": {}}, 400

    if not user.check_password(request.json.get("password")):
        return {"msg": "登录失败!密码错误!", "data": {}}, 400

    return {"msg": "登录成功", "data":{"id": user.id, "name": user.name}}, 200

进入数据库终端,创建一个数据库.

create database pytest;

项目根目录下,单独创建run.py文件,启动mock server,代码:

import config
from api import init_app

# 注意,务必把模型models的内容以及 views 中的服务端接口引入当前文件,否则flask不识别。
from api import models
from api import views

app = init_app()

if __name__ == '__main__':
    app.run(host=config.API_HOST, port=config.API_PORT)

提交代码版本,代码:

git add .
git commit -m "feature: 基于flask实现mockserver"
# 推送代码记录到远程服务器的代码仓库
git push origin master
编写测试用例测试mockserver

tests/users/test_login.py,代码:

import allure
import config
from logger import LogHandle
from requestor import Request


logger = LogHandle().get_logger()
SERVER_URl = f"http://{config.API_HOST}:{config.API_PORT}"

@allure.epic(config.WEB_NAME)
@allure.feature("用户模块")
@allure.story("登录")
class TestLogin(object):
    @allure.severity(allure.severity_level.CRITICAL)
    def test_username_by_empty(self,):
        allure.dynamic.title("用户名为空,登陆失败")
        allure.dynamic.description("测试用户名为空的登陆结果")

        # 发送请求
        request = Request()
        request("POST", f"{SERVER_URl}/user/login", json={
            "username": "",
            "password": "123456"
        })

    def test_password_by_empty(self,):
        allure.dynamic.title("密码为空,登陆失败")
        allure.dynamic.description("测试密码为空的登陆结果")

        # 发送请求
        request = Request()
        request("POST", f"{SERVER_URl}/user/login", json={
            "username": "xiaoming",
            "password": ""
        })
 

提交代码版本,代码:

git add .
git commit -m "test: 编写测试用例测试mockserver"
# 推送代码记录到远程服务器的代码仓库
git push origin master

基于数据驱动生成用例代码

在实际测试开发中,我们一般使用参数化来自动生成测试用例,前面介绍过常用的有ddt与parametrize。那么在pytest中,因为本身提供了parametrize参数化,所以我们往往会在unittest中采用ddt来实现参数化,而在pytest中采用内置的parametrize即可。

而参数化所需要的测试用例,一般我们也是可以采用json,yaml或Excel文件来存储,如果用例数量太多,还可以改成数据库保存。

YAML

YAML(递归缩写:YAML Ain’t a Markup Language,译作:YAML 不是一种标记语言)是一种可读性高,用来表达数据序列化的数据格式,使用场景与xml、json类似,2001年首次发布,在最初开发YAML语言时YAML的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。后面之所以改了名称,原因是为了强调YAML语言以数据为中心,而不是以标记语言为重点。

语法特点

1. 大小写敏感
2. 使用缩进表示嵌套层级关系,且缩进不允许使用tab,只允许使用空格缩进(缩进的空格数不重要,只要相同层级的元素左对齐即可)
3. '#' 表示注释
4. '~' 表示 空(None, null),也可以使用null表示None,但是尽量不要在属性中出现null或者~
5. yaml文件中,属性唯一不能重复,否则报错
6. 文件扩展名为yml或yaml,如:data.yaml 或者 data.yml

test.yaml,代码:

username: 'xiaoming'
age: 16

数据类型

类型名称对应python的数据类型描述
纯量(scalars) 整型、浮点型、布尔型、字符串、None 单个的、不可再分的值
数组(Array) 列表、元祖 一组按次序排列的值,又称为序列(sequence) / 列表(list)
对象(object) 字典 键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
纯量

纯量是最基本的,不可再分的值。类似python中的字符串、布尔值、整型、浮点型、None等

# 纯量
# 字符串
name: 小明
# 字符串内容太多可以换行,yaml中针对源码中的连续多个空格或换行,只会识别成一个空格
title: 如果内容太长,则可以换行
       换行后,两段字符串会被使用空格拼接
       但注意,缩进左对齐,否则报错
# yaml中的属性声明,建议采用python变量的命名规范
content_type: "*/*"  # 如果字符串内容中出现特殊符号,建议加上引号,否则有时候会报错

# 布尔值 False / True
sex: TRUE  # true,True 都可以

# 浮点型
score: 13.5
point: 6.8523015e+5   # 支持科学计数法,也可以6.8523015E+5

# 整型
age: 13
goods_id: 100_200_300  # 支持千分位

# 日期与时间
date: 2022-10-01
time: 12:30:00
datetime: 2018-02-17T15:02:31+08:00

# 空
data1: null
data2: ~
null: ~  # 避免这种情况,不能使用null作为属性名!
数组

一组按次序排列的值,又称为序列(sequence) / 列表(list),类似python的列表。

# 数组
# 数组的成员可以是任意的数据类型
# 方式1:使用中括号进行数组声明,中括号可以换行也可以不换行,无需左对齐
arr1: ["A", "B", True, 1, 1.3]  # 最终还是数组,属性名叫tuple,但并不代表他是元组。
arr2: [
  1,
  2,
  3
]

# 方式2:使用-标记成员进行数组声明,数组成员必须缩进左对齐,否则报错,并且-与成员值必须使用空格隔开
arr3:
  - 1
  - 2
  - 3

# 多层数组嵌套[类似python的多维列表],这里只演示2层,还可以声明更多层嵌套
arr4: [
  [1,2,3,4,5],
  [6,7,8,9,10]
]

arr5:
  -
    - 1.a
    - 1.b
  -
    - 2.a
    - 2.b

对象

键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary),类似python中的字典,但比字典灵活。

# 对象
# 对象的成员是键值对格式数据,属性与属性之间必须使用英文冒号(:)来映射,同时每一个之间使用英文逗号(,)隔开
# 方式1:使用花括号声明,花括号可以换行,无需左对齐
dict: {name: xiaoming, age: 17}
map: {
  name: xiaohong,
  age: 16,
  sex: false
}

# 方式2:使用缩进键值对成员进行对象声明,对象成员必须缩进左对齐,不对齐则报错
data:
  name: xiaoming
  age: 16

# 多层嵌套对象[类似python的多维字典]
people:
  name: xiaoming
  age: 26
  love:
    - code
    - watching TV
    - shopping
  son:
    name: xiaohui
    age: 3

goods_list:
  -
    name: 立白洗衣粉
    price: 39.90
    num: 3
  -
    name: 黑猫神蚊香液
    price: 56.50
    num: 6

python操作YAML

安装yaml模块

pip install pyyaml

基本使用

import yaml
"""读取yaml文件的数据"""
# with open("./data.yaml", "r",encoding="utf-8") as f:
#     content = f.read()
#     data = yaml.load(content, Loader=yaml.FullLoader)
#     print(data)
#     print(data["name"])


"""把数据写入yaml文件"""
from datetime import datetime


with open("./data2.yaml", "w", encoding="utf-8") as f:
    # yaml.dump(data, f, Dumper=yaml.SafeDumper) # 没有多字节内容的情况下
    data = {
        "name": "xiaoming",
        "age": 17,
        "datetime": datetime.now(),
        "point": 1.245464E10,
        "goods_list": [
            {"name": "xiaoming","age": 17, "sex": True},
            {"name": "xiaoming","age": 17, "sex": True},
            {"name": "xiaoming","age": 17, "sex": True},
            {"name": "xiaoming","age": 17, "sex": True},
        ],
        "author_list": ["小明", "小白", "小红"],
        "user_info": {"username":"小明", "password": "123456"}
    }
    yaml.dump(data, f, Dumper=yaml.SafeDumper, allow_unicode=True)  # 有多字节内容的情况下,中文就是多字节内容

封装yaml工具类,utils/yamler.py,代码:

import yaml
from logger import LogHandle


class Yaml(object):
    """yaml操作工具类"""
    __instance = None

    def __new__(cls, *args, **kwargs):
        if not cls.__instance:
            print("创建Yaml的单例")
            cls.__instance = super(Yaml, cls).__new__(cls, *args, **kwargs)
        return cls.__instance

    def __init__(self):
        self.logger = LogHandle().get_logger()

    def read(self, path):
        """读取yaml文件"""
        with open(path, encoding="utf-8") as f:
            result = f.read()
            if result:
                result = yaml.load(result, Loader=yaml.FullLoader)
            return result

    def write(self, path, data):
        """写入yaml文件"""
        try:
            with open(path, "w", encoding="utf-8") as f:
                yaml.dump(data, f, Dumper=yaml.SafeDumper, allow_unicode=True)
            return True
        except Exception as e:
            self.logger(f"写入数据到yaml文件失败:{e}")
            return False


if __name__ == '__main__':
    ya = Yaml()
    data = ya.read("../demo/yaml_demo/data.yaml")
    print(data, type(data))

提交代码版本,代码:

git add .
git commit -m "feature: 封装yaml工具类"
# 推送代码记录到远程服务器的代码仓库
git push origin master

基于yaml数据驱动生成测试用例

data/user_login.yaml,代码:

-
  name: 用户登录-测试用户名与密码为空的情况
  request:
    method: post
    url: /user/login
    json:
      "username": ""
      "password": ""
  assert:
    - code == 400

-
  name: 用户登录-测试密码为空的情况
  request:
    method: post
    url: /user/login
    json:
      "username": "xiaoming"
      "password": ""
  assert:
    - code == 400


-
  name: 用户登录-测试有账号密码但是密码错误的情况
  request:
    method: post
    url: /user/login
    json:
      "username": "xiaoming"
      "password": "1234562222"
  assert:
    - code == 400


-
  name: 用户登录-测试有账号密码单而且正确的情况
  request:
    method: post
    url: /user/login
    json:
      "username": "xiaoming"
      "password": "123456"
  assert:
    - code == 200

utils/assertor.py,代码:

from logger import LogHandle

logger = LogHandle().get_logger()


def assertor(assert_list, response):
    """断言函数"""
    if type(assert_list) is not list:
        assert_list = [assert_list]

    for expr in assert_list:
        logger.info(f"开始断言:assert {expr}")
        if expr:
            exec(f"assert {expr}", {
                "code": response.status_code,
                "json": response.json(),
                "text": response.text,
                "content": response.content,
                "headers": response.headers,
            })

        logger.info(f"断言通过:assert {expr}")

if __name__ == '__main__':
    # Response就是模拟requests HTTP请求工具的返回结果对象
    class Response(object):
        status_code = 400
        text = "对不起,登陆失败!"
        content = "对不起,登陆失败!"
        headers = []

        @classmethod
        def json(cls):
            return {"id": 1},

    assert_list = [
        "code == 400",
        "'失败'in text",
    ]

    assertor(assert_list, Response())

tests/users/test_login.py,代码:

import allure
import config
import pytest
from logger import LogHandle
from requestor import Request
from yamler import  Yaml
from assertor import assertor

logger = LogHandle().get_logger()
SERVER_URl = f"http://{config.API_HOST}:{config.API_PORT}"
yaml = Yaml()


@allure.epic(config.WEB_NAME)
@allure.feature("用户模块")
@allure.story("登录")
class TestLogin(object):
    # @allure.severity(allure.severity_level.CRITICAL)
    # def test_username_by_empty(self,):
    #     allure.dynamic.title("用户名为空,登陆失败")
    #     allure.dynamic.description("测试用户名为空的登陆结果")
    #
    #     # 发送请求
    #     request = Request()
    #     request("POST", f"{SERVER_URl}/user/login", json={
    #         "username": "",
    #         "password": "123456"
    #     })
    #
    # def test_password_by_empty(self,):
    #     allure.dynamic.title("密码为空,登陆失败")
    #     allure.dynamic.description("测试密码为空的登陆结果")
    #
    #     # 发送请求
    #     request = Request()
    #     request("POST", f"{SERVER_URl}/user/login", json={
    #         "username": "xiaoming",
    #         "password": ""
    #     })

    @pytest.mark.parametrize("kwargs", yaml.read(config.BASE_DIR / "data/user_login.yaml"))
    def test_login(self, kwargs):
        request = Request()
        allure.dynamic.title(kwargs.get('name'))
        request.logger.info(f"开始请求测试接口:{kwargs.get('name')}")
        data = kwargs.get('request')
        response = request(data.get("method"), f'{SERVER_URl}{data.get("url")}', json=data.get("json"))
        assertor(kwargs.get("assert"), response)

提交代码版本,代码:

git add .
git commit -m "feature: 基于yaml数据驱动生成测试用例"
# 推送代码记录到远程服务器的代码仓库
git push origin master

Excel

在测试开发中,如果测试用例数量太多,使用yaml也存在很大的维护成本,此时可以考虑使用Excel或者数据库保存更多的测试用例,python中操作Exeel文件的模块有很多,常用的有:xlrd+xlwt,pyexcel+openpyxl等等。

安装模块

pip install xlrd
pip install xlwt

封装Excel工具类

utils/excel.py,代码:

import xlrd, json

class Excel(object):
    """Excel文件操作工具类"""
    def __init__(self, filename):
        self.workbook = xlrd.open_workbook(filename, formatting_info=True)

    def get_sheet_names(self):
        """
        获取当前excel文件所有的工作表的表名
        :return:
        """
        return self.workbook.sheet_names()

    def __get_sheet(self, sheet_index_or_name):
        """
        根据sheet的索引或名称,获取sheet对象
        :param sheet_index_or_name: sheet的索引或名称
        :return:sheet对象
        """
        if isinstance(sheet_index_or_name, int):
            if len(self.workbook.sheet_names()) > sheet_index_or_name:
                return self.workbook.sheet_by_index(sheet_index_or_name)
            else:
                raise Exception("Invalid Sheet Index!")
        elif isinstance(sheet_index_or_name, str):
            if sheet_index_or_name in self.workbook.sheet_names():
                return self.workbook.sheet_by_name(sheet_index_or_name)
            else:
                raise Exception("Invalid Sheet Name!")

    def get_rows_num(self,sheet_index_or_name):
        """
        获取指定工作表的数据总行数
        :param sheet_index_or_name: 工作表名或索引
        :return:
        """
        return self.__get_sheet(sheet_index_or_name).nrows

    def get_cols_num(self,sheet_index_or_name):
        """
        获取指定工作表的数据总列数
        :param sheet_index_or_name: 工作表名或索引
        :return:
        """
        return self.__get_sheet(sheet_index_or_name).ncols

    def get_cell_value(self, sheet_index_or_name, row_index, col_index):
        """
        获取指定工作表的指定位置的数据值
        :param sheet_index_or_name: 工作表名或索引
        :param row_index: 行下标,从0开始
        :param col_index: 列下标,从0开始
        :return:
        """
        sheet = self.__get_sheet(sheet_index_or_name)
        if sheet.nrows and sheet.ncols:
            return sheet.cell_value(row_index, col_index)
        else:
            raise Exception("Index out of range!")

    def get_data(self, sheet_index_or_name, fields, first_line_is_header=True):
        """
        获取工作表的所有数据
        :param sheet_index_or_name: 工作表名或索引
        :param fields: 返回数据的字段名
        :param first_line_is_header: 工作表是否是否表头,也就是非数据
        :return:
        """
        rows = self.get_rows_num(sheet_index_or_name)
        cols = self.get_cols_num(sheet_index_or_name)
        data = []
        for row in range(int(first_line_is_header), rows):
            row_data = {}
            for col in range(cols):
                cell_data = self.get_cell_value(sheet_index_or_name, row, col)
                if type(cell_data) is str and ("{" in cell_data and "}" in cell_data or "[" in cell_data and "]" in cell_data):
                    """判断如果表格中填写的数据是json格式键值对,则采用json模块转换成字典"""
                    cell_data = json.loads(cell_data)
                row_data[fields[col]] = cell_data
            data.append(row_data)

        return data

if __name__ == '__main__':
    xls = Excel("../data/case_user.xls")
    fields = [
        "case_id",
        "module_name",
        "case_name",
        "method",
        "url",
        "headers",
        "params_desc",
        "params",
        "assert_result",
        "real_result",
        "remark",
    ]

    print(xls.get_data(0, fields))

"""
[
    {'case_id': 1.0, 'module_name': '用户模块', 'case_name': '用户登录-测试用户名为空的情况', 'method': 'post', 'url': 'http://127.0.0.1:8000/user/login', 'headers': '', 'params_desc': 'username: 用户名\npassword: 密码', 'params': {'username': '', 'password': '123456'}, 'assert_result': 'code==400', 'real_result': '', 'remark': ''}, 
    {'case_id': 2.0, 'module_name': '用户模块', 'case_name': '用户登录-测试密码为空的情况', 'method': 'post', 'url': 'http://127.0.0.1:8000/user/login', 'headers': '', 'params_desc': 'username: 用户名\npassword: 密码', 'params': {'username': 'xiaoming', 'password': ''}, 'assert_result': 'code==400', 'real_result': '', 'remark': ''}, 
    {'case_id': 3.0, 'module_name': '用户模块', 'case_name': '用户登录-测试账号密码正确的情况', 'method': 'post', 'url': 'http://127.0.0.1:8000/user/login', 'headers': '', 'params_desc': 'username: 用户名\npassword: 密码', 'params': {'username': 'xiaoming', 'password': '123456'}, 'assert_result': 'code==200', 'real_result': '', 'remark': ''}, 
    {'case_id': 4.0, 'module_name': '用户模块', 'case_name': '用户登录-测试使用手机号码登录', 'method': 'post', 'url': 'http://127.0.0.1:8000/user/login', 'headers': '', 'params_desc': 'username: 手机号\npassword: 密码', 'params': {'username': '13312345678', 'password': '123456'}, 'assert_result': 'code==200', 'real_result': '', 'remark': ''}]

"""

config.py,代码:

import pathlib  # 路径操作模块,替代 os.path模块,os.path采用字符串来操作路径,pathlib采用面向对象来操作路径

# 项目目录的主目录路径[字符串路徑]
BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix()  # 基本操作系统转换路径的分隔符 as_posix
# 項目目录的主目录路径[路径对象]
BASE_DIR = pathlib.Path(BASE_DIR_STR)

# 项目名
WEB_NAME = "路飞自动化接口测试框架-master"

# 测试自动化项目的运行端口与IP地址
HOST = "127.0.0.1"
PORT = 8088

"""日志配置"""
LOGGING = {
    "name": "luffytest",  # 日志处理器的名称,一般使用当前项目名作为名称
    "filename": (BASE_DIR / "logs/luffytest.log").as_posix(),  # 日志文件存储路径,注意,一定要在项目根目录下手动创建logs目录
    "charset": "utf-8",  # 日志内容的编码格式
    "backup_count": 31,  # 日志文件的备份数量
    "when": "d",   # 日志文件的创建间隔事件,m 表示每分钟创建1个,h表示每小时创建1个,d表示每天创建1个,m0~m6表示每周星期日~星期六创建1个,midnight表示每日凌晨
}


# excel测试用例字段格式
FIELD_LIST = [
    "case_id",        # 用例编号
    "module_name",    # 模块名称
    "case_name",      # 用例名称
    "method",         # 请求方式
    "url",            # 接口地址
    "headers",        # 请求头
    "params_desc",    # 参数说明
    "params",         # 请求参数
    "assert_result",  # 预期结果
    "real_result",    # 实际结果
    "remark",         # 备注
]


"""mock server 的服务端配置"""
# 数据库连接
# SQLALCHEMY_DATABASE_URI: str = "数据库类型://账户:密码@IP地址:端口/数据库名称?charset=utf8mb4"
SQLALCHEMY_DATABASE_URI: str = "mysql+pymysql://root:123@127.0.0.1:3306/pytest?charset=utf8mb4"
# 查询时会显示原始SQL语句
SQLALCHEMY_ECHO: bool = True

# 调试模式
DEBUG = True
# 监听端口
API_PORT = 8000
# 监听地址
API_HOST = "127.0.0.1"

提交代码版本,代码:

git add .
git commit -m "feature: 封装Excel工具类"
# 推送代码记录到远程服务器的代码仓库
git push origin master

基于excel文件实现数据驱动生成测试用例

tests/users/user_login.py,代码:

import allure
import config
import pytest
from logger import LogHandle
from requestor import Request
from yamler import  Yaml
from assertor import assertor
from utils.excel import Excel

logger = LogHandle().get_logger()
SERVER_URl = f"http://{config.API_HOST}:{config.API_PORT}"
yaml = Yaml()


@allure.epic(config.WEB_NAME)
@allure.feature("用户模块")
@allure.story("登录")
class TestLogin(object):
    # @allure.severity(allure.severity_level.CRITICAL)
    # def test_username_by_empty(self,):
    #     allure.dynamic.title("用户名为空,登陆失败")
    #     allure.dynamic.description("测试用户名为空的登陆结果")
    #
    #     # 发送请求
    #     request = Request()
    #     request("POST", f"{SERVER_URl}/user/login", json={
    #         "username": "",
    #         "password": "123456"
    #     })
    #
    # def test_password_by_empty(self,):
    #     allure.dynamic.title("密码为空,登陆失败")
    #     allure.dynamic.description("测试密码为空的登陆结果")
    #
    #     # 发送请求
    #     request = Request()
    #     request("POST", f"{SERVER_URl}/user/login", json={
    #         "username": "xiaoming",
    #         "password": ""
    #     })

    # @pytest.mark.parametrize("kwargs", yaml.read(config.BASE_DIR / "data/user_login.yaml"))
    # def test_login(self, kwargs):
    #     """数据驱动自动化测试-基于yaml生成"""
    #     request = Request()
    #     allure.dynamic.title(kwargs.get('name'))
    #     request.logger.info(f"开始请求测试接口:{kwargs.get('name')}")
    #     data = kwargs.get('request')
    #     response = request(data.get("method"), f'{data.get("url")}', json=data.get("json"))
    #     assertor(kwargs.get("assert"), response)

    @pytest.mark.parametrize("kwargs", Excel(config.BASE_DIR / "data/case_user.xls").get_data(0, config.FIELD_LIST))
    def test_login(self, kwargs):
        """数据驱动自动化测试-基于excel生成"""
        request = Request()
        allure.dynamic.title(kwargs.get('case_name'))
        request.logger.info(f"开始请求测试接口:{kwargs.get('case_name')}")
        if kwargs.get("method").lower() in ["get", "delete"]:
            """发送get或delete"""
            response = request(kwargs.get("method"), f'{kwargs.get("url")}', params=kwargs.get("params"))
        else:
            """发送post,put,patch"""
            response = request(kwargs.get("method"), f'{kwargs.get("url")}', json=kwargs.get("params"))
        assertor(kwargs.get("assert_result"), response)

提交代码版本,代码:

git add .
git commit -m "feature: 基于excel数据驱动生成测试用例"
# 推送代码记录到远程服务器的代码仓库
git push origin master

认证测试

在用户注册/登陆以后,往往项目会返回登陆状态(jwt,session, cookie)提供给客户端,所以上面我们所实现的mockserver实际上是有问题的。因此接下来我们继续来模拟存在jwt认证鉴权的服务端,并在测试框架中基于conftest.py来实现认证测试的这个流程。

 

安装jwt

pip install flask-jwt-extended

config.py,代码:


# 秘钥,不管是使用session还是jwt认证,都需要对认证的信息鉴权加密
SECRET_KEY = "ac361a52518d99f4525c1cfe5ba635572190aa6ac52bc8f27ae1b07529feafd0"

api/__init__.py,代码:

import config
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager

# SQLAlchemy初始化
db = SQLAlchemy()

app = Flask(__name__)

jwt = JWTManager()

def init_app():
    """服务端初始化"""
    # 加载配置
    app.config.from_object(config)
    # 加载mysql数据库配置
    db.init_app(app)

    # jwt初始化
    jwt.init_app(app)

    # 自动创建数据表
    with app.app_context():
        db.create_all()

    return app

api/views.py,代码:

from flask import request
from sqlalchemy import or_
from flask_jwt_extended import jwt_required, create_access_token

from . import app
from .models import User,db


@app.route("/user/register", methods=["POST"])
def register():
    """
    用户信息注册
    :return:
    """
    try:
        data = request.json
        # 创建用户数据
        user = User(**data)
        db.session.add(user)
        db.session.commit()
        return {"msg": "注册成功!", "data": {"id":user.id, "name": user.name}}, 200
    except Exception as e:
        return {"msg": "注册失败!", "data": {}}, 400


@app.route("/user/login", methods=["POST"])
def login():
    """
    用户登录
    :return:
    """
    user = User.query.filter(
        or_(
            User.mobile == request.json.get("username"),
            User.name == request.json.get("username"),
            User.email == request.json.get("username")
        )
    ).first()  # 实例化模型

    if not user:
        return {"msg": "登录失败!用户不存在!", "data": {}}, 400

    if not user.check_password(request.json.get("password")):
        return {"msg": "登录失败!密码错误!", "data": {}}, 400

    # 生成token,并返回给客户端
    access_token = create_access_token(identity={"id": user.id, "username": user.name})
    return {"msg": "登录成功", "data": {"token": access_token}}, 200


@app.route("/user", methods=["GET"])
@jwt_required()   # 当前这个装饰器的作用就是 校验用户是否登录
def user_center():
    return {"errmsg": "访问成功"}

项目根目录下创建conftest.py,代码:

import pytest
import config
from utils.requestor import Request
from utils.yamler import Yaml

yaml = Yaml()


@pytest.fixture(scope="class", autouse=False)
def jwt_token():
    request = Request()
    request.logger.info("获取token")
    data = yaml.read(config.BASE_DIR / "data/test_user.yaml")
    response = request(data.get("method"), data.get("url"), json=data.get("json"))
    token = response.json().get("data", {}).get("token")
    yield token
    # 生成器函数中的暂停关键字,作用是当代码运行到yield时,把yield右边的数据作为返回值提供给调用处,把代码执行权交出去。
    request.logger.info("移除token")

data/test_user.yaml,填写一个保存正确用户信息的用例,方便在conftest中发送正确的账户信息获取token,代码:

method: post
url: http://127.0.0.1:8000/user/login
json:
  "username": "xiaoming"
  "password": "123456"

新建一个测试用例,tests/test_user.py,代码:

import allure
import pytest
import config
from utils.requestor import Request
from utils.yamler import Yaml
from utils.assertor import assertor


yaml = Yaml()


@allure.epic(config.WEB_NAME)
@allure.feature("用户模块")
@allure.story("用户中心")
class TestUser(object):
    @pytest.mark.usefixtures("jwt_token")
    @pytest.mark.parametrize("kwargs", yaml.read(config.BASE_DIR / "data/user_info.yaml"))
    def test_user(self, jwt_token, kwargs):
        allure.dynamic.title(kwargs.get('name'))
        request = Request()
        request.logger.info(f"开始请求测试接口:{kwargs.get('name')}")
        data = kwargs.get('request')
        data['headers']["Authorization"] = data['headers']["Authorization"].format(token=jwt_token)
        response = request(data.get("method"), data.get("url"), headers=data.get("headers"))
        assertor(kwargs.get("assert"), response)

data/user_info.yaml,代码:

-
  name: 用户中心-测试jwt认证鉴权
  request:
    method: get
    url: http://127.0.0.1:8000/user
    headers:
      Authorization: "Bearer {token}"
  assert:
    - code == 200

提交代码版本,代码:

git add .
git commit -m "feature: 基于conftest实现jwt认证鉴权的测试用例"
# 推送代码记录到远程服务器的代码仓库
git push origin master

UI自动化测试

UI测试(User Interface Test,译作:用户界面测试),UI测试侧重产品的UI交互是否正确,模拟后端进行测试也可以,放在单元测试里去做也可以。

Web自动化测试

常用的web自动化测试工具有:selenium,Playwright、QTP、Cypress、Rapise、TestProject、Ranorex、Katalon、Subject7、Serenity等等

selenium

 

Selenium是由杰森·哈金斯(Jason Huggins)于2004年在思特沃克(ThoughtWorks)开发的一款专门为Web应用而开发的自动化测试工具(最早是作为火狐浏览器的一个插件存在,内核基于javascript实现), 适合进行功能测试、验收测试,同时支持所有基于web的管理任务自动化。简单来说,就是用程序模拟人操作浏览器网页,可以实现UI自动化测试。

Selenium基于webdriver协议可以控制浏览器模仿用户操作完成UI自动化测试,同时它也是一个生态系统,其主要功能包括:测试与浏览器的兼容性,测试应用程序看是否能够很好得工作在不同浏览器和操作系统之上。测试系统功能,创建回归测试检验软件功能和用户需求。Selenium包含了Selenium IDE、Selenium RC(Selenium2.0以后已经webdriver替代了)、Selenium Grid。

Selenium发展到今天已经经历4个版本:Selenium1.0,Selenium2.0,Selenium3.0,Selenium4.0

特点:

  • 开源(使用Apache License 2.0协议发布)、免费、简单易学、操作灵活、功能齐全。
  • 支持市面上基本所有的浏览器:FirefoxChrome、IE(7, 8, 9, 10, 11)、Edge、Opera、**Safari**等等。
  • 跨平台:windows、linux、mac
  • 多语言支持:javapython、Ruby、C#、JavaScript、C++
  • 对web页面有良好的支撑。
  • 支持分布式执行测试用例。

官方文档:https://www.selenium.dev/documentation/

安装步骤

  1. 安装要测试的浏览器的webdriver插件
  2. 安装对应编程语言的开发工具模块

下载地址:https://www.selenium.dev/downloads/

webdriver插件安装
浏览器类型下载地址
Chrome http://chromedriver.storage.googleapis.com/index.html
Firefox https://github.com/mozilla/geckodriver/releases
IE http://selenium-release.storage.googleapis.com/index.html
Edge https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/
PhantomJS(无头浏览器) https://phantomjs.org/

> 注意:UI自动测试依赖于浏览器环境,所以浏览器务必不要设置为自动更新!!!下载的浏览器webdriver驱动一定要对应当前测试的浏览器版本。

选择点击同样版本的webdriver目录

 

根据自己的操作系统下载对应的webdriver版本。

 

快速使用

python中安装selenium

pip install selenium
打开浏览器
import time

from selenium import webdriver
from selenium.webdriver.chrome.service import Service

# # 旧版本selenium指定webdriver.exe的路径,操作对应的浏览器
# driver = webdriver.Chrome(executable_path="./chromedriver.exe")
# print(driver)

# 旧版本selenium指定webdriver.exe的路径,初始化一个谷歌浏览器实例对象
service = Service(executable_path="./chromedriver.exe")

# driver浏览器驱动对象,是一个连接对象,提供了操作浏览器的具体的功能方法以及与浏览器相关的功能参数等等
driver = webdriver.Chrome(service=service)
print(driver)

# 浏览器最小化窗口
driver.minimize_window()

# 通过driver.get方法打开一个网址,相当于新建一个窗口
driver.get("https://www.luffycity.com")

time.sleep(5)

# 关闭浏览器
driver.quit()

窗口操作
操作描述
driver.set_window_size(宽度, 高度) 设置浏览器窗口大小,单位:px 像素
driver.get_window_size() 获取浏览器窗口大小,返回结果为字典格式
driver.get(url地址) 根据url地址打开一个新的浏览器窗口,或当前窗口跳转到地址url地址页面
driver.current_url 获取当前浏览器窗口的url地址
driver.title 获取当前浏览器窗口的页面标题
driver.forward() 浏览器前进下一页
driver.back() 浏览器返回上一页
driver.refresh() 刷新当前浏览器
driver.close() 关闭当前浏览器窗口,如果浏览器只有一个标签页,则表示关闭浏览器
driver.quit() 关闭浏览器
import time

from selenium import webdriver
from selenium.webdriver.chrome.service import Service

service = Service(executable_path="./chromedriver.exe")
driver = webdriver.Chrome(service=service)
# 设置浏览器的宽高
driver.set_window_size(1555, 768)
# 获取浏览器的宽高
ret = driver.get_window_size()
print(ret) # {'width': 1555, 'height': 770}
driver.get("https://www.luffycity.com")
print(f"网页标题:{driver.title}")
print(f"当前地址:{driver.current_url}")
time.sleep(3)
# 刷新浏览器当前页面
driver.refresh()
time.sleep(3)
# 刷新浏览器当前页面
driver.refresh()
time.sleep(3)

# 在当前窗口打开一个新页面
driver.get("https://www.baidu.com")
time.sleep(3)
# 返回上一步访问的url地址,相当于返回路飞
driver.back()
time.sleep(3)
# 前进到下一步访问的url地址,相当于前进到百度
driver.forward()

# 等待10秒退出,如果当前窗口只有一个浏览器页面,则表示退出当前浏览器
time.sleep(10)
driver.close()
元素定位

我们通过webdriver打开一个网络页面,目的是为了操作当前页面已完成浏览器中的一些UI测试步骤,所以必然需要操作网页。而网页的内容组成是由HTML标签(element,也叫元素),所以基于selenium操作网页实际上本质就是操作元素。那么要操作元素就必须先获取元素对象。selenium中关于元素的获取也叫元素的定位,提供了8种元素定位操作可以让测试人员获取一个或多个HTML元素。

定位类型低版本代码新版本代码描述
xpath driver.find_element_by_xpath(“xpath”)<br>driver.find_elements_by_xpath(“xpath”) driver.find_element(By.XPATH, “xpath”)<br>driver.find_elements(By.XPATH, “xpath”) 基于xpath路径表达式对HTML元素进行提取操作
selector driver.find_element_by_css_selector(“css_selector”)<br>driver.find_elements_by_css_selector(“css_selector”)


 

 
posted @ 2024-09-06 14:31  甜甜de微笑  阅读(7)  评论(0编辑  收藏  举报