Pytest-BDD实现接口自动化测试,并附全部代码
引言
在之前的文章中简单的介绍了怎么使用Pytest-BDD进行接口测试,可以参考《pytest-bdd 行为驱动自动化测试》。本篇文章主要介绍使用Pytest-BDD实现接口自动化测试。后面的文章会介绍生成测试报告,和流程性接口测试。
feature文件
首先我们先整理好一个接口测试,需要用到的场景、参数等信息,写到feature文件中,以便我们根据各种测试中的行为来进行脚本实现。
demo.feature
Feature: 测试模块Scenario: demo接口测试Given 初始化Given 登录When 调用 "/test" 接口# 这里只配置接口路径,具体的域名和IP,通过config文件配置,以便用了切换环境When 使用 "post" 请求# 目前支持的请求方式 post get put deleteWhen 参数类型 "json"# 目前支持的参数类型 params json form-data form-urlencodedWhen 请求头 "{'Content-type': 'application/json'}"When 参数 "{'id': '1'}"When 校验类型 "包含校验"# 支持的校验类型: 包含校验 相等校验 字段类型校验 字段值校验 状态码校验When 校验文本 "成功"When 校验字段类型为 "int"# 字段类型:int str float list dict boolWhen 校验字段 "loginInfo"When 上传文件 "test.txt"When 退出Then 调用成功
从上面看出,我们要实现的行为有初始化、登录,步骤中需要实现接口地址、请求方式、参数和断言。目前我们需要的接口测试行为已经列出来,下面就是对这些行为的编码实现。
py文件
首先,我们来实现feature文件中的{初始化}。将feature文件中的参数进行初始化。初始化调用api工具的函数。
class ApiTest:def __init__(self):self.api = Noneself.methods = 'get'self.data_type = 'params'self.params = {}self.headers = {}self.test_result_type = Noneself.result_text = Noneself.result_type = Noneself.result_data_type = Noneself.result_key = Noneself.file = Noneself.is_logout = Falsedef call_api(self, test_body):response = api_test(test_body)return response@pytest.fixture
@given('初始化')
def api_tool():return ApiTest()
实现feature文件中的{登录}。登录的目的是拿到接口的授权,所以我们调用登录方法,拿到鉴权后,将鉴权保存到headers中,以便后面进行接口请求时使用。具体实现登录的方法就不展示了,根据自己的项目来实现就可以。
@given('登录')
def login(api_tool):headers = get_token()for key, value in headers.items():api_tool.headers[key] = value
实现feature文件中{调用 “/test” 接口},拿到feature文件中的接口路径,然后赋值给api,后面接口工具会根据环境和接口路径拼接成完整的接口地址。
@when(parsers.parse('调用 "{url}" 接口'))
def api(api_tool, url):api_tool.api = url
实现feature文件中{使用 “post” 请求},拿到feat文件中的请求方法,赋值给methods,后面接口工具使用该请求方法。
@when(parsers.parse('使用 "{methods}" 请求'))
def methods(api_tool, methods):api_tool.methods = methods
实现feature文件中{参数类型 “json”}
@when(parsers.parse('参数类型 "{data_type}"'))
def data_type(api_tool, data_type):api_tool.data_type = data_type
实现feature文件中{请求头 “{‘Content-type’: ‘application/json’}”},因为通过feature文件拿到的参数都是字符串类型,所以要将字符串转成dict,并添加到请求头中。
@when(parsers.parse('请求头 "{headers}"'))
def headers(api_tool, headers):headers_dict = ast.literal_eval(headers)for key, value in headers_dict.items():api_tool.headers[key] = value
实现feature文件中{参数 “{‘id’: ‘1’}”},将字符串类型的参数转成dict。
@when(parsers.parse('参数 "{params}"'))
def params(api_tool, params):api_tool.params = ast.literal_eval(params)
实现feature文件中{校验类型 “包含校验”},根据映射关系,将校验类型数据赋值给test_result_type。
test_result_type_list = {'相等校验': 0,'包含校验': 1,'字段类型校验': 2,'字段值校验': 3,'状态码校验': 4
}@when(parsers.parse('校验类型 "{test_result_type}"'))
def result_test(api_tool, test_result_type):api_tool.test_result_type = test_result_type_list[test_result_type]
实现feature文件中的{校验文本 “成功”}、{校验字段类型为 “int”}、{校验字段 “loginInfo”},这三个加上校验类型,是断言工具需要的,如果是相当校验,会将测试接口返回的response和result_text进行相等校验,如果是包含校验,会校验测试接口的response是否包含result_text,如果是字段类型校验,会校验result_key的类型是否为result_type,如果是字段值校验,会校验测试接口的response返回的result_key的value值,是否等于result_text,如果是状态码校验,会校验response的状态码,是否为result_text。
@when(parsers.parse('校验文本 "{result_text}"'))
def result_text(api_tool, result_text):api_tool.result_text = result_text@when(parsers.parse('校验字段类型为 "{result_type}"'))
def result_type(api_tool, result_type):api_tool.result_type = result_type@when(parsers.parse('校验字段 "{result_key}"'))
def result_key(api_tool, result_key):
实现feature文件中{When 上传文件 “test.txt”},这个步骤只有接口需要上传文件时,才需要写,上传文件放到项目的statics目录下。
@when(parsers.parse('上传文件 "{files}"'))
def file(api_tool, files):api_tool.file = files
实现feature文件中的{退出},is_logout为True时,调用完测试接口会执行退出登录操作。
@when(parsers.parse("退出"))
def logout(api_tool):api_tool.is_logout = True
实现feature文件中{调用成功},将上面步骤保存的参数,拼装成两个dict,test_data为断言工具需要的参数,test_body为接口请求需要的参数,然后调用封装好的接口测试工具和断言工具,进行接口测试。
@then('调用成功')
def asserts(api_tool):test_data = {'test_result': {'text': api_tool.result_text, 'type': api_tool.result_type, 'result_type': api_tool.result_type, 'key': api_tool.result_key}}test_body = {'URL': api_tool.api, 'method': api_tool.methods, 'data_type': api_tool.data_type, 'headers': api_tool.headers, 'params': api_tool.params, 'file': api_tool.file}response = api_tool.call_api(test_body)if api_tool.is_logout:do_logout()if api_tool.test_result_type is not None:assert_tool(response, test_data)
登录和退出的函数就不介绍了,根据自己的项目来实现就可以,主要介绍一下接口测试工具和断言工具。
接口测试工具
接口测试工具中,读取配置文件的参数,放到文章最后,会把测试文件的内容,还有yaml工具的代码附上。
import requests
import os
import json
from common.yaml_tool import read_yamldef api_test(test_body):config = read_yaml('config.yaml')config_env = config['env']data_type = test_body['data_type'].lower()url = config_env + test_body['URL']method = test_body['method'].lower()data = test_body['params']headers = test_body['headers']if headers is None:headers = {}file = test_body['file']if data_type.lower() == 'params':response = params(url, method, data, headers)elif data_type == 'json':response = json(url, method, data, headers)elif data_type == 'form-data':response = formdata(url, method, data, headers, file)elif data_type == 'form-urlencoded':response = urlencoded(url, method, data, headers)else:return {'code': '5000', 'msg': '测试脚本暂时不支持的参数类型'}# print('接口返回内容:' + response.text)return responsedef params(url, method, data, headers):try:if method == 'post':response = requests.post(url, params=data, headers=headers)elif method == 'put':response = requests.put(url, params=data, headers=headers)elif method == 'get':response = requests.get(url, params=data, headers=headers)elif method == 'delete':response = requests.delete(url, params=data, headers=headers)else:response = {'code': '5000', 'msg': '测试脚本暂时不支持的请求方法'}response.encoding = 'utf-8'return responseexcept Exception as e:return {"code": "5000", "msg": str(e)}def json(url, method, data, headers):try:if method == 'post':response = requests.post(url, data=data, headers=headers)elif method == 'put':response = requests.put(url, data=data, headers=headers)elif method == 'get':response = requests.get(url, data=data, headers=headers)elif method == 'delete':response = requests.delete(url, data=data, headers=headers)else:response = {'code': '5000', 'msg': '测试脚本暂时不支持的请求方法'.encode('utf-8')}return responseexcept Exception as e:return json.dumps({'code': '5000', 'msg': '测试脚本报错:' + str(e)})def urlencoded(url, method, data, headers):try:if method == 'post':response = requests.post(url, data=data, headers=headers)elif method == 'put':response = requests.put(url, data=data, headers=headers)elif method == 'get':response = requests.get(url, data=data, headers=headers)elif method == 'delete':response = requests.delete(url, data=data, headers=headers)else:response = {'code': '5000', 'msg': '测试脚本暂时不支持的请求方法'.encode('utf-8')}return responseexcept Exception as e:return {'code': '5000', 'msg': '测试脚本报错:' + str(e)}def formdata(url, method, data, headers, file):if file:file_name = filename = os.path.splitext(file_name)[-1]if name == '.doc':file_type = 'application/msword'elif name == '.docx':file_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'elif name == '.pdf':file_type = 'application/pdf'elif name == '.jpg' or name == 'jpeg':file_type = 'image/jpeg'elif name == '.png':file_type = 'image/png'elif name == '.ppt':file_type = 'application/vnd.ms-powerpoint'elif name == '.xls':file_type = 'application/vnd.ms-excel'elif name == '.xlsx':file_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'elif name == '.txt':file_type = 'text/plain'path = os.path.dirname(os.path.dirname(__file__))file = [('file', ('Test'+name, open(path + '/statics/' + file_name, 'rb'), file_type))]try:if method == 'post':response = requests.post(url, data=data, files=file, headers=headers)elif method == 'put':response = requests.put(url, data=data, files=file, headers=headers)elif method == 'get':response = requests.get(url, data=data, files=file, headers=headers)elif method == 'delete':response = requests.delete(url, data=data, files=file, headers=headers)else:response = {'code': '5000', 'msg': '测试脚本暂时不支持的请求方法'.encode('utf-8')}return responseexcept Exception as e:return {'code': '5000', 'msg': '测试脚本报错:' + str(e)}
断言工具
import json
from requests import Responsedef assert_tool(response, test_result):response_body = Noneif type(response) is dict:response_body = responseelse:response_body = response.texttext = test_result['test_result']['text']types = test_result['test_result']['type']type_map = {'int': int,'float': float,'str': str,'list': list,'dict': dict,'bool': bool,}if types == 0:assert text == str(response_body)elif types == 1:assert text in str(response_body)elif types == 2:result_type = test_result['test_result']['result_type']key_str = test_result['test_result']['key']assert isinstance(analysis_dict(json.loads(response_body), key_str), type_map.get(result_type))elif types == 3:key_str = test_result['test_result']['key']assert text == str(analysis_dict(json.loads(response_body), key_str))elif types == 4:if type(response) is Response:status = response.status_codeassert text == statuselse:assert text == str(response_body)def analysis_dict(response: dict, key_str):if key_str in response:return response[key_str]for key, value in response.items():if isinstance(value, dict):result = analysis_dict(value, key_str)if result is not None:return resultelif isinstance(value, list):for item in value:if isinstance(item, dict):result = analysis_dict(item, key_str)if result is not None:return resultreturn None
本接口自动化测试工具,基本实现了接口测试和断言。后面会加上测试报告,还有流程性接口测试(如接口2的参数,需要从接口1中获取)
配置文件
env: http://yourdomin.com
login:URL: /loginheaders:content-type: application/jsonparams:username: usernamepassword: password
logout:URL: /logoutheaders:content-type: application/json
yam文件工具
import yamldef read_yaml(yaml_file):with open(yaml_file, 'r', encoding='utf-8') as file:test_data = yaml.safe_load(file)return test_datadef wirte_config(config, config_file):with open(config_file, 'w', encoding='utf-8') as file:yaml.dump(config, file, default_flow_style=False)
python文件
实现feature文件的整体python文件
import pytest
from pytest_bdd import scenarios, given, when, then, parsersfrom common.api_tool import api_test
from common.token_api import get_token, get_cms_token, do_logout
from common.assert_tool import assert_tool
import ast
import ostest_result_type_list = {'相等校验': 0,'包含校验': 1,'字段类型校验': 2,'字段值校验': 3,'状态码校验': 4
}class ApiTest:def __init__(self):self.api = Noneself.methods = 'get'self.data_type = 'params'self.params = {}self.headers = {}self.test_result_type = Noneself.result_text = Noneself.result_type = Noneself.result_data_type = Noneself.result_key = Noneself.file = Noneself.is_logout = Falsedef call_api(self, test_body):response = api_test(test_body)return responsefor root, dirs, files in os.walk('case/'):for case in files:scenarios(root + case)@pytest.fixture
@given('初始化')
def api_tool():return ApiTest()@given('登录')
def login(api_tool):headers = get_token()for key, value in headers.items():api_tool.headers[key] = value@when(parsers.parse('调用 "{url}" 接口'))
def api(api_tool, url):api_tool.api = url@when(parsers.parse('使用 "{methods}" 请求'))
def methods(api_tool, methods):api_tool.methods = methods@when(parsers.parse('参数类型 "{data_type}"'))
def data_type(api_tool, data_type):api_tool.data_type = data_type@when(parsers.parse('请求头 "{headers}"'))
def headers(api_tool, headers):headers_dict = ast.literal_eval(headers)for key, value in headers_dict.items():api_tool.headers[key] = value@when(parsers.parse('参数 "{params}"'))
def params(api_tool, params):api_tool.params = ast.literal_eval(params)@when(parsers.parse('校验类型 "{test_result_type}"'))
def result_test(api_tool, test_result_type):api_tool.test_result_type = test_result_type_list[test_result_type]@when(parsers.parse('校验文本 "{result_text}"'))
def result_text(api_tool, result_text):api_tool.result_text = result_text@when(parsers.parse('校验字段类型为 "{result_type}"'))
def result_type(api_tool, result_type):api_tool.result_type = result_type@when(parsers.parse('校验字段 "{result_key}"'))
def result_key(api_tool, result_key):api_tool.result_key = result_key@when(parsers.parse('上传文件 "{files}"'))
def file(api_tool, files):api_tool.file = files@when(parsers.parse("退出"))
def logout(api_tool):api_tool.is_logout = True@then('调用成功')
def asserts(api_tool):test_data = {'test_result': {'text': api_tool.result_text, 'type': api_tool.result_type, 'result_type': api_tool.result_type, 'key': api_tool.result_key}}test_body = {'URL': api_tool.api, 'method': api_tool.methods, 'data_type': api_tool.data_type, 'headers': api_tool.headers, 'params': api_tool.params, 'file': api_tool.file}response = api_tool.call_api(test_body)if api_tool.is_logout:do_logout()if api_tool.test_result_type is not None:assert_tool(response, test_data)
相关文章:
Pytest-BDD实现接口自动化测试,并附全部代码
引言 在之前的文章中简单的介绍了怎么使用Pytest-BDD进行接口测试,可以参考《pytest-bdd 行为驱动自动化测试》。本篇文章主要介绍使用Pytest-BDD实现接口自动化测试。后面的文章会介绍生成测试报告,和流程性接口测试。 feature文件 首先我们先整理好…...

Sqli-labs-master靶场--布尔盲注
目录 1、布尔盲注 2、布尔盲注的流程(以靶场less-8为例) 2.1输入id尝试是否存在注入点 2.1.1通过以上尝试,联想到可能是布尔盲注 2.2猜测数据库长度 2.3获取数据库名 2.3.1python脚本获取 代码: 获取结果为: …...
【QGroundControl二次开发】十. QT添加GStreamer视频播放同时保存
上一章介绍使用QT播放GStreamer视频流 【QGroundControl二次开发】八. QT实现播放gstreamer视频。 这章介绍如何在原有基础上保存为视频,同时保存为一个个规定大小的小视频。 先展示代码: #include <QApplication> #include <QWidget> #include <QtConcurrent…...

double类型 精度丢失的问题
前言 精度丢失的问题是在其他计算机语言中也都会出现,float和double类型的数据在执行二进制浮点运算的时候,并没有提供完全精确的结果。产生误差不在于数的大小,而是因为数的精度。 一、double进行运算时,经常出现精度丢失 0.10.2使用计算…...

C++ 重要特性探究
shared_from_this 使用分析 场景 类的成员函数需要获取指向自身的shared_ptr的时候类成员函数传递shared_ptr给其他函数或者对象的时候,目的是为了管理对象生命周期使用方法 首先类必须继承 std::enable_shared_from_this<T>必须使用 shared_from_this 获取指…...
c++_游戏_狼人杀
思路主要包括以下几个部分: 角色分配:代码中通过随机数的方式给狼人、平民、预言家和法师等角色进行分配,保证每个角色的数量和身份的随机性。 游戏进行:根据狼人、平民、预言家和法师等角色的身份,游戏进行了夜晚和白…...

MySQL——数据类型、索引的建立、数据的约束
文章目录 数据类型索引的建立普通索引唯一索引使用ALTER 命令添加和删除索引使用ALTER 命令添加和删除主键显示索引信息 数据的约束非空约束:not null,值不能为null唯一约束:unique,值不能重复主键约束:primary key外键…...

常见框架漏洞详解③!!
Apache Apache 是世界使⽤排名第⼀的 Web 服务器软件。它可以运⾏在⼏乎所有⼴泛使⽤的计算 机平台上,由于其跨平台和安全性被⼴泛使⽤,是最流⾏的 Web 服务器端软件之⼀。 apache⽬录结构: bin:存放常⽤命令⼯具,如h…...
大数据基础知识
大数据(Big Data)是指无法用传统数据处理工具和技术有效处理的大规模、复杂的数据集。大数据技术通过对这些数据进行存储、处理和分析,从中提取有价值的信息和见解。 1. 大数据的特点 大数据通常具有以下四个主要特点,被称为“4…...

SQL Server 的透明数据加密
透明数据加密是SQL Server数据库安全众多特性中的一个,本文只针对透明数据加密。 在此测试之前,已经按照文档如何快速获得一个测试用SQL Server企业版创建了一个SQL Server 2019,并按照文档为SQL Server安装示例数据库AdventureWorks安装了…...
Windows图形界面(GUI)-MFC-C/C++ - 列表视图(List Control) - CListCtrl
公开视频 -> 链接点击跳转公开课程博客首页 -> 链接点击跳转博客主页 目录 列表视图(List Control) - CListCtrl 创建列表视图 设置列表视图属性 成员函数 注意事项 示例代码 列表视图(List Control) - CListCtrl 创建列表视图 在对话框编辑器中ÿ…...

一机两用的简单介绍
电子政务外网终端使用过程的风险与挑战 1、终端防护弱,失陷风险大 政务外网终端具备访问互联网能力,造成政务外网终端极易感染僵木蠕病毒,破坏正常办公 政务外网终端易被攻击失陷,成为从互联网攻击政务外网的跳板机 2、VPN漏洞…...

uniapp离线打包热更新失败-AndroidStudio离线打包apk后无法下载打开-热更新失败-plus.runtime.install失败
效果图 仅安卓 前言 1.plus.runtime.install一直fail(20240808), uni.openDocument可以打开本地apk文件 2.权限问题需小心 跑通前提 1.先确定apk地址有效,浏览器中手动下载可安装 2.确保已添加离线打包AndroidStudio的“android.permission.INSTALL_PACKAGES”权…...

深植根基、蓬勃向上 | openKylin 2.0正式发布!
2024年8月8日,openKylin 2.0版本正式发布!该版本默认搭载Linux 6.6 LTS内核,完成180操作系统核心组件自主选型升级,深度融合AI技术,上线麒麟AI助手等实用AI功能,并为用户带来包括开明软件包格式、不可变系统…...

【Material-UI】按钮组:尺寸与颜色详解
文章目录 一、按钮组概述1. 组件介绍2. 基本用法 二、按钮组的尺寸(Sizes)1. 小尺寸(Small)2. 中等尺寸(Medium)3. 大尺寸(Large) 三、按钮组的颜色(Colors)1…...

app抓包 burp配置
证书导出 模拟器安装证书 点击安装证书 将证书直接拖进来就行 配置代理 打开浏览器抓包...

图像与像素:利用ImageJ分析荧光显微镜图像|QuPath基础教程1|24-08-08
内容概要 数字图像由像素组成,每个像素具有一个数值,通常与检测到的光线相关。相同的像素值可以通过不同的方式进行显示。在科学图像处理中,可以通过修改查找表来独立于像素值改变图像外观。 一、引言 图像由其最小的组成单位——像素构成。…...

Prompt Fuzzer:用于增强 GenAI 应用程序的开源工具
Prompt Fuzzer 是一个开源工具,可以评估GenAI应用程序的系统提示针对基于动态 LLM 的威胁的安全性。 Prompt Fuzzer 功能: 1. 模拟十几种类型的 GenAI 攻击。 2. 该工具会根据系统提示自动进行情境化,针对与 GenAI 应用程序相关的特定主题或行…...

Vision Pro使用GLFT 加载模型shader错误解决办法
Glft shader在vision pro上加载错误 前言相关背景解决办法 参考文章 前言 之前在Vision Pro上尝试加载Glb文件,但是加载完成后发现加载出来的Glb文件材质不正确。材质是黑色的。因此整理一下解决方案。 相关背景 使用Unity开发,Glb的加载插件为gltf F…...

Netty技术全解析:MessageToMessageCodec类深度解析
❃博主首页 : 「码到三十五」 ,同名公众号 :「码到三十五」,wx号 : 「liwu0213」 ☠博主专栏 : <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关> ♝博主的话 :…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
Xen Server服务器释放磁盘空间
disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...

springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...

MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...