传统软件测试行业是以手工测试为主,也就是所谓的点点点,加上国内软件公司不注重测试,受制于大环境影响等也就给了大众一种测试人员虽然身处互联网行业,却是毫无技术可言的工种。
话锋一转,到了如今,不得不说一声:大人,时代变了,最直观的表现莫过于招聘要求的提高,越来越要求测试人员拥有七十二变的能力。
而在这其中,自动化测试能力是现在手工测试迈向更高技术岗位的必经之路。
搭建接口测试框架
构建接口测试思维:
当前互联网产品最大的特点就是快,上线周期通常是以"天"甚至是以"小时"为单位,而传统软件产品的周期多以"月",甚至以"年"为单位。因此,如何在保证产品质量下,有效缩短测试回归时间成了重中之重。
两个突破口:
引入测试的并发执行,即从以往的串行执行测试用例,采用分布式的方法并行执行。
从测试策略上找到突破口,从传统软件产品的金字塔测试策略往菱形测试策略转变。以接口测试为主,GUI测试为辅,单元测试则根据公司实际情况进行。
四点建议:
以中间层的API测试为重点做全面的测试;
轻量级的GUI测试,只覆盖最核心直接影响主营业务流程的E2E场景;
最上层的GUI测试通常利用探索式测试思维,以人工测试的方式发现尽可能多的潜在问题;
单元测试只对那些相对稳定并且核心的服务和模块开展全面的单元测试,而应用层或者上层业务只会做少量的;
为何要搭建测试框架?
开发自己的框架更能结合自身工作中的痛点,难点来做一个针对性的解决,使其扩展性更高,后期也能接入CI/CD。
利用现有工具来进行接口测试,随着项目的规模变大,维护成本将会增大,不利于管控。
工具本身具有一定的局限性,如支持的协议比较单一。
不用纠结技术选型,根据自身的技术实力和技术功底来选择,而不要以开发工程师的技术栈来选择。
定义专属框架目录结构
test_case:存放测试用例
test_data:存放测试数据
report:存放测试报告
common:存放公共方法
lib:存放第三方库
config:存放环境配置信息
main:框架主入口
fixture:类似unittest中的setUp/tearDown的存在,但功能远比他们强大
构建框架流程
在框架构建过程中,由于篇符有限,本文只涉及其中部分环节。
1、在common公共模块、封装定义框架专属的http请求能力
# !/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: pan-li
import requests
class HttpRequests(object):
def __init__(self, url):
self.url = url
self.req = requests.session()
# 自定义请求头,根据自身所在公司项目需求
self.headers = {'Content-Type': 'application/json', 'User-Agent': 'Node midway-v2x Version/1.28.1'}
# 封装get请求
def get(self, url='', params='', data='', headers=None, cookies=None):
response = self.req.get(url=url, params=params, data=data, headers=headers, cookies=cookies)
return response
# post请求
def post(self, url='', params='', data='', headers=None, cookies=None):
response = self.req.post(url=url, params=params, data=data, headers=headers, cookies=cookies)
return response
# put请求
def put(self, url='', params='', data='', headers=None, cookies=None):
response = self.req.put(url=url, params=params, data=data, headers=headers, cookies=cookies)
return response
# delete请求
def delete(self, url='', params='', data='', headers=None, cookies=None):
response = self.req.delete(url=url, params=params, data=data, headers=headers, cookies=cookies)
return response
2、抽离URL生成url_conf.py在config文件中
import enum
class URLConf(enum.Enum):
TEST_URL = 'http://10.12.7.20:8443/v2x-omp/api/'
3、编写接口测试用例在test_case文件中,第一版测试用例,安装pytest,pip install -U pytest
import os
import sys
import pytest
import json
from common.http_requests import *
from config.url_conf import URLConf
project_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
sys.path.append(project_root)
class TestV2x:
@classmethod
def setup_class(cls) -> None:
cls.url = URLConf.TEST_URL.value
cls.http = HttpRequests(cls.url)
def setup(self) -> None:
self.headers = {'Content-Type': 'application/json', 'User-Agent': 'Node midway-v2x Version/1.28.1'}
self.http = HttpRequests(self.url)
def tearDown(self):
pass
@staticmethod
def get_token():
headers = {'Content-Type': 'application/json', 'User-Agent': 'Node midway-v2x Version/1.28.1'}
response = TestV2x.http.post(url=URLConf.TEST_URL.value, data='{"cmd":"signin","params":{"userName":"smarttest","password":"72be4b7f62832c516b85fb26de59df53"}}', headers=headers)
token = response.json()['detail']['token']
return token
def test_001_queryArea(self):
"""查询区域"""
playload = {"cmd": "queryArea", "csrfToken": TestV2x.get_token(), "params": {"cityId": "320200"}}
response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers)
resultNote = response.json().get('resultNote')
assert resultNote, 'Success'
def test_002_queryYearlyCheckCount(self):
"""查询年检总数"""
playload = {"cmd": "queryYearlyCheckCount", "Token": TestV2x.get_token(), "params": {}}
response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers)
resultNote = response.json().get('resultNote')
assert resultNote, 'SUCCESS'
def test_003_queryTrafficEvent(self):
"""查询交通事件"""
playload = {"cmd": "queryTrafficEvent", "Token": TestV2x.get_token(), "params": {}}
response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers)
resultNote = response.json().get('resultNote')
assert resultNote, 'Success'
def test_004_queryRsuCount(self):
"""查询rsu总数"""
playload = {"cmd": "queryRsuCount", "Token": TestV2x.get_token(), "params": {}}
response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers)
resultNote = response.json().get('resultNote')
assert resultNote, '查询路测设备数量成功!'
def test_005_queryDeviceDetail(self):
"""查询设备详情"""
playload = {"cmd": "queryDeviceDetail", "params": {"deviceId": '0086860703231572'}, "Token": TestV2x.get_token()}
response = TestV2x.http.post(self.url, data=json.dumps(playload), headers=self.headers)
resultNote = response.json().get('resultNote')
assert resultNote, '查询终端信息成功!'
if __name__ == '__main__':
pytest.main()
4、显然前面的测试用例也是流水账似的,还有很大的优化空间,现在就来一步一步进行。
5、优化一:利用feature特性优化前置和后置条件,fixture目录下的v2x_fixture.py文件
import pytest
from common.http_requests import HttpRequests
from config.url_conf import URLConf
@pytest.fixture(scope='function', autouse=True)
def http():
url = URLConf.TEST_URL.value
http = HttpRequests(url)
return http
@pytest.fixture(scope='function', autouse=True)
def get_token(http):
headers = {'Content-Type': 'application/json', 'User-Agent': 'Node midway-v2x Version/1.28.1'}
response = http.post(url=URLConf.TEST_URL.value,
data='{"cmd":"signin","params":{"userName":"smarttest","password":"72be4b7f62832c516b85fb26de59df53"}}',
headers=headers)
token = response.json()['detail']['token']
return token
上述在引入feature之后,简化了http请求的调用,重新定义http()来进行调用。之前每次接口的调用都要附带token参数,现在把获取token的方法提取出来,单独封装,加上feature的装饰,他会作用与每一个方法,用起来更加方便。此处的token是依赖登陆接口之后返回的值,可根据自身项目的需求封装。
6、优化二: 为测试用例添加数据驱动模式
# 以第五个测试用例单独为例
@pytest.mark.parametrize('deviceid', ['0086860703231572', '0086337601270714', '0086822412608154'])
def test_005_queryDeviceDetail(self, http, get_token, deviceid):
"""查询设备详情"""
playload = {"cmd": "queryDeviceDetail", "params": {"deviceId": deviceid}, "Token": get_token}
response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value)
resultNote = response.json()
assert resultNote.get('resultNote'), '查询终端信息成功!'
logger.info('查询终端信息成功!')
"""直接利用pytest.mark.parametrize()装饰器,第一个参数为参数名,后边数组为测试数据,用例当中同样添加形参deviceid"""
在 pytest 中,数据驱动是经由 pytest 自带的 pytest.mark.parametrize() 来实现的。 pytest.mark.parametrize 是 pytest 的内置装饰器,它允许你在 function 或者 class 上定义多组参 数和 fixture 来实现数据驱动。
@pytest.mark.parametrize() 装饰器接收两个参数:
第一个参数以字符串的形式存在,它代表能被被测试函数所能接受的参数,如果被测试函数有多个参数, 则以逗号分
第二个参数用于保存测试数据。如果只有一组数据,以列表的形式存在,如果有多组数据,以列表嵌套元 组的形式存在
7、优化三: 为测试用例添加标签,此时用到pytest.ini配置文件,放在项目任意位置都能生效,有以下作用:
为你的测试框架定制用例查找规则;
为你的测试框架注册标签名称;
指定查找用例起始目录;
[pytest]
python_files = test_* *_test test*
python_classes = Test* test*
python_functions = test_* test*
markers =
smoke: marks tests as smoke
test : marks tests as test
log : marks tests as log
# 使用时只需要在测试用例上使用@pytest.mark.smoke即可
# 执行时pytest -m [标记名]
8、优化四: 配置pytest.ini文件集成日志收集和实时控制台打印功能
[pytest]
log_cli = 1
log_cli_level = DEBUG
log_cli_date_format = %Y-%m-%d-%H-%M-%S
log_cli_format = %(asctime)s - %(filename)s - %(name)s - %(module)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s
log_file = ..\\report\\run.log
log_file_level = DEBUG
log_file_date_format = %Y-%m-%d-%H-%M-%S
log_file_format = %(asctime)s - %(filename)s -%(name)s - %(module)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s
关于字段的详解可以在终端输入pytest --help 查看
9、优化五: 定制测试框架测试报告,属于第三方应用放在lib目录中
这里我们使用目前市面上使用人数较多的一款开源测试报告框架Allure,它支持绝大多数测试框架
安装方法:
pip install -U allure-pytest
github上下载最新版本放到lib目录,并配置成系统环境变量(: https://github.com/allure-framework/allure2/releases)
使用方法:
执行pytest命令,并指定allure报告目录: pytest -v -s test_v2x_api_02.py --alluredir=./allure_reports
在线生成allure报告:allure serve allure_reports
生成本地allure报告:allure generate allure_reports
当然这只是在控制台直接命令执行,还不够方便,如果我们想在其他环境运行就又得配置环境变量,那么我们如何把它集成到我们的框架中呢
在共同方法中生成allure工具类,以便分辨运行环境是windows还是mac
import os
import sys
import platform
path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'lib')
allure_path = os.path.join(path, 'allure', 'bin')
sys.path.append(allure_path)
class Report():
@property
def allure(self):
if platform.system() == 'Windows':
cmd = os.path.join(allure_path, 'allure.bat')
else:
cmd = os.path.join(allure_path, 'allure')
return cmd
10、在main模块中,添加执行调度策略
import os
import threading
import pytest
from common.report import Report
project_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
report_dir = os.path.join(project_root, 'report')
result_dir = os.path.join(report_dir, 'allure_result')
allure_report = os.path.join(report_dir, 'allure_report')
report = Report()
def run_pytest():
pytest.main(['-v', '-s', f'--alluredir={result_dir}'])
def general_report():
cmd = "{} generate {} -o {} --clean".format(report.allure, result_dir, allure_report)
print(os.popen(cmd).read())
if __name__ == '__main__':
run = threading.Thread(target=run_pytest)
gen = threading.Thread(target=general_report)
run.start() # 多线程先执行pytest命令生成测试报告
run.join()
gen.start() # 报告生成后调用allure工具类生成本地报告
11、最后一版测试用例,整合前面的优化
import os
import sys
import json
from fixture.v2x_fixture import *
from config.url_conf import URLConf
project_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
sys.path.append(project_root)
class TestV2x:
@pytest.mark.smoke # 标签的使用
def test_001_queryArea(self, http, get_token):
"""查询区域"""
playload = {"cmd": "queryArea", "csrfToken": get_token, "params": {"cityId": "320200"}}
response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value)
resultNote = response.json()
assert resultNote.get('resultNote'), 'success'
logger.info('查询区域成功')
def test_002_queryYearlyCheckCount(self, http, get_token):
"""查询年检总数"""
playload = {"cmd": "queryYearlyCheckCount", "Token": get_token, "params": {}}
response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value)
resultNote = response.json()
assert resultNote.get('resultNote'), 'SUCCESS'
logger.info('查询年检成功')
def test_003_queryTrafficEvent(self, http,get_token):
"""查询交通事件"""
playload = {"cmd": "queryTrafficEvent", "Token": get_token, "params": {}}
response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value)
resultNote = response.json()
assert resultNote.get('resultNote'), 'Success'
logger.info('查询交通事件成功')
def test_004_queryRsuCount(self, http, get_token):
"""查询rsu总数"""
playload = {"cmd": "queryRsuCount", "Token": get_token, "params": {}}
response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value)
resultNote = response.json()
assert resultNote.get('resultNote'), '查询路测设备数量成功!'
# text = response.text
# print(text)
logger.info('查询路侧设备成功')
# 简单的数据驱动
@pytest.mark.parametrize('deviceid', ['0086860703231572', '0086337601270714', '0086822412608154'])
def test_005_queryDeviceDetail(self, http, get_token, deviceid):
"""查询设备详情"""
playload = {"cmd": "queryDeviceDetail", "params": {"deviceId": deviceid}, "Token": get_token}
response = http.post(url=URLConf.TEST_URL.value, data=json.dumps(playload), headers=URLConf.HEADERS.value)
resultNote = response.json()
assert resultNote.get('resultNote'), '查询终端信息成功!'
logger.info('查询终端信息成功!')
if __name__ == '__main__':
# 打印更详细的信息
pytest.main(['-s', '-v', ])
下面是我整理的2023年最全的软件测试工程师学习知识架构体系图 |
只要心中燃烧着不灭的激情,勇敢面对困难与挑战,坚持不懈地追求目标,就能书写属于自己的辉煌篇章。相信自己的能力与潜力,努力奋斗,每一次努力都是一次进步,每一次拼搏都是一次成长。奋斗不息,成功必将绽放!
不论前路多么曲折艰难,只要心怀信念与勇气,坚持不懈地奋斗,才能创造属于自己的辉煌。相信自己的才华与潜力,努力拼搏,每一次奋斗都是一次进步,每一次挑战都是一次成长。
勇往直前,不畏艰难险阻,只有坚持不懈的奋斗,才能追逐梦想的光芒。相信自己的能力与潜力,不断超越自我,每一次努力都是一次成长,每一次拼搏都是一次进步。