💻 杂七杂八面试题
接口和压测工具都有哪些,如何选择?
接口测试工具:
- Postman:适合简单接口测试,不适合复杂场景
- SoapUI:主要用于测试Web Service接口(SOAP协议和RESTful接口)【SOAP现在用的很少】
- JMeter:支持较多协议,但更多作为压测工具使用
- Python/Pytest+requests:适合复杂场景,但需要一定的编程能力
- Java/Rest-Assured:适合Java项目,但需要一定的编程能力
压测工具:(压力测试Load Testing和性能测试Performance Testing)
- JMeter:支持较多协议,可以通过图形化界面或命令行方式执行,使用简单
- LoadRunner:商业级性能测试工具,功能非常全面,支持广泛的协议和技术,能够模拟大规模用户并发,提供详细的性能分析报告。通常用于大型企业级应用的性能测试。
- Locust:基于Python的开源压力测试工具,使用Python代码定义用户行为,易于编写复杂的测试场景,支持分布式压测,并提供实时的 Web UI 监控
选择因素
- 成本/预算
- 团队规模
- 易用性
- 可扩展性(集成其他工具,如监控系统、告警系统)
- 可维护性
- 接口类型
- 项目需求和复杂度、系统技术栈
- 团队技能和熟悉程度
- 是否需要支持多环境
- 是否需要支持多种协议
- 是否需要支持团队协作
- 问题定位
- 测试报告展示
接口测试用例的编写要点有哪些?
理解需求和接口文档
覆盖各种测试场景
- 正常场景:合法入参
- 异常场景:非法入参、类型、长度、范围、必填
- 边界场景:最大/最小值、临界值
- 安全性测试:权限控制、敏感数据加密、SQL注入、XSS攻击【跨站脚本攻击,通常是注入js脚本】
- 性能测试:大批量访问是否有性能瓶颈
关注业务逻辑
错误处理是否合理
可读性、可维护性
测试相关(参数化、数据驱动)
黑盒测试和白盒测试有哪些方法?
黑盒测试(不关注内部结构,只关注输入和输出)
- 等价类划分
- 边界值分析
- 因果图
- 决策表
- 正交试验
- 场景法
- 错误推测
白盒测试(关注内部结构,关注代码逻辑)
- 语句覆盖
- 分支覆盖
- 条件覆盖
- 判定覆盖
- 条件组合覆盖
- 路径覆盖
- 单元测试
Token和Session区别?
状态管理、存储位置
- Token:无状态,服务端不保存会话状态,所有状态都保存在客户端
- Session:有状态,服务端保存会话状态,客户端保存会话ID
可扩展性:
- Token: 本身包含了足够的信息(例如用户身份、权限),服务器在验证时不需要查询额外的存储,因此更易于在分布式环境下进行扩展。每个独立的服务器都可以使用相同的密钥来验证Token。
- Session:可扩展性好,可以用于分布式系统(需要使用共享储存,如Redis,Memcached)
安全性:
- Token:主要的安全风险是Token被窃取,攻击者可以在Token过期之前冒充用户。为了提高安全性,通常会使用HTTPS、设置较短的过期时间、使用Refresh Token等机制。对于JWT,签名算法的安全性也至关重要。
- Session:主要的安全风险是Session Hijacking和Session Fixation。为了减轻这些风险,通常会使用 HTTPS、HTTPOnly和Secure Cookie等措施。
服务器负载:
- Token:服务器不需要存储用户的会话信息,因此可以减少服务器的负载,特别是对于大量用户的应用。
- Session:每个用户都需要一个独立的Session,如果用户数量很多,服务器需要为每个用户维护一个Session,这将导致服务器负载增加。
过期时间:
- Token:通常也会设置过期时间。为了在Token过期后保持用户的登录状态,通常会配合使用Refresh Token。当 Access Token 过期时,客户端可以使用Refresh Token向服务器请求新的Access Token。
- Session:可以设置Session的过期时间,当用户长时间不活动时,Session会失效。
软件测试分为哪几个阶段?
- 需求分析阶段
- 测试计划阶段(编写测试计划)
- 测试设计阶段(编写用例)
- 测试环境准备阶段
- 测试执行阶段
- 单元测试
- 集成测试
- 系统测试
- 验收测试(UAT)
- 回归测试(可选)
- 测试报告阶段
JMeter中跟踪重定向和自动重定向有什么区别?
特性 | 跟踪重定向 (Follow Redirects) | 自动重定向 (Auto Redirects) |
---|---|---|
记录重定向请求 | 会记录每个重定向请求为一个独立的 SampleResult。 | 不会记录原始的重定向请求,只记录最终的请求。 |
测试结果条目 | 结果中会包含原始的 3xx 响应和后续的重定向请求。 | 结果中只包含最终的请求。 |
性能指标 | 可以查看每个重定向步骤的性能指标。 | 只能看到最终请求的性能指标,原始重定向的耗时包含在其中。 |
适用场景 | 需要详细分析重定向过程、验证重定向行为、查看所有相关请求。 | 只关心最终结果、希望测试结果简洁、将重定向视为正常行为。 |
http和https的区别?
核心区别:安全性【https是http的安全增强版】
- http信息是明文传输,https信息是密文传输
- 标准端口号:http是80,https是443
- 证书:http不需要证书,https需要证书
- 性能:https比http性能低
- 成本:https比http成本高
get请求和post请求的区别?
特性 | GET 请求 | POST 请求 |
---|---|---|
主要用途 | 获取资源 | 提交数据,创建或修改资源 |
数据传输位置 | URL 查询参数 (Query Parameters) | 请求体 (Request Body) |
数据可见性 | 可见,安全性较低 | 相对不可见,安全性较高 |
缓存性 | 可以被缓存 | 通常不会被缓存 |
幂等性 | 幂等 | 通常非幂等 |
长度限制 | 受 URL 长度限制,不适合传输大量数据 | 限制较少,适合传输大量数据 |
添加到历史记录 | 会添加到浏览器历史记录 | 通常不会直接添加到浏览器历史记录 |
请求体 | 通常不包含请求体 | 必须包含请求体 |
测试用例的八大(N大)元素
- 测试用例编号
- 所属模块
- 测试用例标题
- 前置条件
- 测试步骤
- 预期结果
- 实际结果
- 优先级
- 用例类型
- 测试环境
- 执行人员
- 备注
算法题:找最小连续的子数组
算法题:一个数组,找出其中最小连续的子数组,使得这个子数组排序后,整体数组是有序的
例:数组[1,3,2,7,5,8],找到子数组[3,2,7,5],使得其排序后[2,3,5,7],整体[1,2,3,5,7,8]有序)
咋一看好像不难,写出了下面的代码:
def find_unsorted_subarray(nums):
length = len(nums)
if length <= 1:
return []
# 寻找右界idx
right = length-1
for i in range(length - 1, 0, -1):
if nums[i - 1] > nums[i]:
right = i - 1
break
right += 1
print(f'right: {right}')
# 寻找左界idx
left = 0
for i in range(0, length - 1):
if nums[i] < nums[i + 1]:
left += 1
break
print(f'left: {left}')
return nums[left: right + 1]
if __name__ == "__main__":
nums = [1,3,2,7,5,8]
print(find_unsorted_subarray(nums))
如上,有问题:
如果数组本身就是有序的,返回有误(应该返回空数组)
只单纯的找到了起始和结束边界,但未进行扩展:考虑这样一种场景,如果子数组中的最小值比左边元素小,最大值比右边元素大,那么就不符合了,因此需要进行扩展边界
参考答案给出的代码如下:【这道题在搜索的时候,gemini和deepssek的回答多次有误,使用grok回答对了..囧😶】
def find_unsorted_subarray(nums):
length = len(nums)
print(f'length: {length}')
if length <= 1:
return []
# 寻找左界
left = 0
while left < length - 1 and nums[left] <= nums[left + 1]:
left += 1
print(f'left: {left}, left num: {nums[left]}')
if left == length - 1:
return []
# 寻找右界
right = length - 1
while right > 0 and nums[right] >= nums[right - 1]:
right -= 1
print(f'right: {right}, right num: {nums[right]}')
# 找到子数组的最小值和最大值
subarray_min = min(nums[left: right + 1])
subarray_max = max(nums[left: right + 1])
print(f'min: {subarray_min}, max: {subarray_max}')
print(nums[left: right + 1])
# 进行扩展:
while left > 0 and nums[left - 1] > subarray_min:
left -= 1
while right < length - 1 and nums[right + 1] < subarray_max:
right += 1
return nums[left: right + 1]
Python的内存管理机制是什么?
1. 引用计数 (Reference Counting)
这是Python最主要的内存管理方式。
Python中的每个对象都会维护一个引用计数器,用于记录当前有多少个变量或对象引用着它。
增加引用计数的情况:
- 创建一个新的对象并将其赋值给一个变量时,该对象的引用计数会增加。
- 将一个对象作为参数传递给函数时,该对象在函数内部的引用计数会增加。
- 将一个对象存储到容器(如列表、字典、元组)中时,该对象在容器中的引用计数会增加。
减少引用计数的情况:
- 当一个变量被赋予新的值时,之前引用的对象的引用计数会减少。
- 当一个包含对象的容器被销毁时,该容器中所有对象的引用计数都会减少。
- 当一个函数执行完毕,函数内部使用的局部变量引用的对象的引用计数会减少。
- 使用 del 语句显式删除一个变量时,该变量引用的对象的引用计数会减少。
优点: 简单高效,能够及时回收不再使用的内存。
缺点: 无法解决循环引用的问题。例如,两个对象互相引用,即使它们不再被其他对象引用,它们的引用计数也不会变为零,导致内存泄漏。
2. 垃圾回收 (Garbage Collection)
为了解决引用计数无法处理的循环引用问题,Python引入了垃圾回收机制。
垃圾回收器会定期扫描内存中的对象,检测是否存在循环引用,并回收这些不再使用的对象。
Python的垃圾回收机制主要基于标记-清除 (Mark and Sweep) 算法。
- 标记阶段: 从根对象(例如全局变量、活动函数的局部变量等)开始,遍历所有可达的对象,并将它们标记为“存活”。
- 清除阶段: 遍历整个堆内存,将没有被标记为“存活”的对象清除掉,释放其占用的内存。
为了提高垃圾回收的效率,Python还引入了分代回收 (Generational Garbage Collection) 的策略。
- Python将内存中的对象分为三个“代”(generation):0代、1代和2代。
- 新创建的对象属于第0代。
- 如果在一次垃圾回收中对象仍然存活,它会被移动到下一代。
- 垃圾回收器会更频繁地回收第0代的对象,因为新创建的对象更容易变成垃圾。而对于存活时间较长的对象(高代),回收频率会降低。
- 这样做的目的是减少垃圾回收的开销,提高程序的整体性能。
3. 内存池机制 (Memory Pools / Object Allocator)
对于频繁使用的小块内存,Python引入了内存池机制进行优化。
Python的内存管理机制在底层会维护多个内存池,用于不同大小的对象分配。
当需要分配小于一定大小(通常是512字节)的对象时,Python会尝试从对应的内存池中获取空闲的内存块,而不是直接向操作系统申请内存。
优点:减少了频繁向操作系统申请和释放内存的开销,提高了内存分配和释放的效率。
Python的内存池机制是分层的:
- Level 1: 用于分配小的对象,如整数、短字符串等。
- Level 2: 用于分配中等大小的对象。
- Level 3: 直接使用操作系统的内存分配。
总结:
- 引用计数 是主要机制,负责大部分对象的及时回收。
- 垃圾回收 作为补充,处理引用计数无法解决的循环引用问题,并采用分代回收优化性能。
- 内存池机制 则针对小对象进行优化,提高内存分配效率。
TCP、UDP区别
特性 | TCP | UDP |
---|---|---|
连接性 | 连接导向 (Connection-Oriented) | 无连接 (Connectionless) |
可靠性 | 可靠传输 (Reliable Transmission) | 不可靠传输 (Unreliable Transmission) |
有序性 | 有序传输 (Ordered Delivery) | 无序传输 (Unordered Delivery) |
流量控制 | 有 (Yes) | 无 (No) |
拥塞控制 | 有 (Yes) | 无 (No) |
头部大小 | 较大 (Higher) | 较小 (Lower) |
传输速度 | 相对较慢 (Relatively Slower) | 较快 (Faster) |
适用场景 | 需要可靠数据传输的应用 | 对实时性要求高的应用,可容忍少量数据丢失 |
TCP的三次握手:
- 第一次握手:客户端发送SYN包到服务器,等待服务器确认
- 第二次握手:服务器收到SYN包,确认客户端的SYN,并发送SYN+ACK包到客户端
- 第三次握手:客户端收到SYN+ACK包,确认服务器的SYN,并发送ACK包到服务器
2次不够,无法保证服务器知道客户端已接收到,也无法保证客户端知道服务器已接收到 4次不行,浪费资源(增加延迟和开销)
浏览器输入一个网址,接下来会发生什么?
- URL解析
- DNS查询获取目标服务器IP地址
- 建立TCP连接(如果是HTTPS还会进行TLS/SSL握手)
- 浏览器发送HTTP请求
- 服务器处理请求
- 服务器发送HTTP响应
- 浏览器解析和渲染响应内容
- (可能)关闭TCP连接
HTTPS数字证书的认证过程:
- 服务器发送证书(证书信息:公钥、证书颁发机构、有效期、域名)
- 浏览器验证证书的合法性
- 检查证书是否由受信任的证书颁发机构(CA)签发
- 检查证书是否过期
- 检查证书是否与请求的域名匹配
- 如果证书合法,浏览器生成一个随机数,并使用证书中的公钥加密该随机数
- 浏览器将加密后的随机数发送给服务器
- 服务器使用私钥解密得到随机数
- 浏览器和浏览器使用该随机数进行对称加密通信
OSI7层网络模型
① 物理层:
负责在物理介质上传输原始的比特流(0和1)。它定义了物理连接的特性,如电缆类型、接口规范、信号传输速率等。
- 以太网、WiFi、蓝牙、USB..
② 数据链路层:
负责在直接相连的两个节点之间提供可靠的数据传输。它将物理层提供的比特流组织成帧(frame),并处理MAC地址寻址、错误检测和纠正等。
- 以太网中的MAC子层协议、点对点PPP、虚拟局域网VLAN、地址解析一些ARP
③ 网络层:
负责在源主机和目标主机之间提供逻辑寻址和路由选择。它定义了IP地址,并负责将数据包从源网络路由到目标网络。
- 互联网协议IP、互联网控制报文协议ICMP、路由协议
④ 传输层:
负责在应用程序之间提供端到端的可靠或不可靠的数据传输服务。它处理数据的分段、重组、流量控制和拥塞控制。
- 传输控制协议TCP、用户数据报协议UDP
⑤ 会话层:
负责建立、管理和终止应用程序之间的会话(连接)。它提供会话控制和同步等服务。
- 点对点隧道协议PPTP、
⑥ 表示层:
负责数据的格式化、加密和压缩,以确保应用程序可以理解接收到的数据。它处理数据在不同系统之间的转换。
- 安全套接字层/传输层安全协议SSL/TLS、图像相关JPEG/MPEG
⑦ 应用层:
为最终用户提供网络应用程序接口。这是用户直接与之交互的层次。
- 超文本传输协议HTTP、安全超文本传输协议HTTPS、文件传输协议FTP、简单邮件传输协议FTP、邮局协议版本3 POP3、互联网消息访问协议IMAP、域名系统DNS、安全外壳协议SSH、远程终端协议Telnet、
TCP/IP模型:将OSI的会话层和表示层合并为应用层,将数据链路层和网络层合并为网络接口层。 即:网络接口层、互联网层、传输层、应用层
栈STACK和堆HEAP的区别
特征 | 栈 (Stack) | 堆 (Heap) |
---|---|---|
用途 | 主要用于存储函数调用时的局部变量、参数、返回地址等 | 主要用于动态分配内存,存储程序运行时创建的对象和数据 |
数据结构 | 线性数据结构,后进先出 (LIFO - Last-In, First-Out) | 无序的树状结构,不连续分配,可以随机分配和释放内存块 |
管理方式 | 由编译器自动管理,分配和释放由系统自动完成 | 由程序员手动管理(分配和释放),或者由垃圾回收机制负责 |
分配方式 | 连续分配,后进先出 (LIFO - Last-In, First-Out) | 不连续分配,可以随机分配和释放内存块 |
内存大小 | 通常较小,大小在编译时确定或由操作系统限制 | 通常较大,理论上受限于系统可用内存 |
分配效率 | 非常快,只需要移动栈指针 | 相对较慢,需要查找合适的空闲内存块 |
碎片问题 | 不容易产生内存碎片 | 容易产生内存碎片,可能导致内存利用率不高 |
生命周期 | 变量的生命周期通常与其所在的函数或代码块相同 | 对象的生命周期由程序员或垃圾回收机制控制 |
访问速度 | 访问速度快 | 访问速度相对较慢 |
进程、线程的区别
- 定义:
- 进程(Process): 是操作系统中资源分配的基本单位。一个进程可以包含一个或多个线程。每个进程都拥有独立的内存空间(地址空间)、数据段、代码段以及其他系统资源(如打开的文件、网络连接等)。可以将其理解为一个正在运行的应用程序的实例。
- 线程(Thread): 是操作系统中CPU 调度的基本单位。一个线程存在于一个进程之内,是进程中实际执行的指令流。一个进程中的多个线程共享该进程的内存空间、数据段和代码段等资源,但拥有自己独立的栈空间和程序计数器。可以将其理解为进程中不同的执行路径。
- 资源拥有:
- 进程: 拥有独立的资源,包括内存空间、文件句柄、I/O 设备等。创建和销毁进程的开销较大,因为需要分配和回收这些资源。
- 线程: 不拥有独立的系统资源,而是共享其所属进程的资源。创建和销毁线程的开销相对较小。
- 上下文切换:
- 进程切换: 当操作系统从一个进程切换到另一个进程时,需要保存当前进程的 CPU 状态(如寄存器值、程序计数器等)和内存状态,然后加载下一个进程的状态。这个过程开销较大。
- 线程切换: 同一个进程内的线程切换只需要保存和恢复线程的 CPU 状态(如寄存器值、程序计数器等),由于它们共享内存空间,不需要切换内存状态,因此开销较小。
- 通信:
- 进程间通信(IPC - Inter-Process Communication): 由于进程拥有独立的内存空间,进程之间的通信需要使用特定的机制,如管道、消息队列、共享内存、信号量、套接字等,这些机制通常涉及内核的介入,开销相对较大。
- 线程间通信: 同一个进程内的线程可以直接共享进程的内存空间,因此线程之间的通信非常方便和高效。但也需要注意同步机制(如互斥锁、信号量、条件变量等)来避免竞态条件。
- 开销:
- 进程: 创建、销毁和切换进程的开销都比线程大,因为涉及到独立的资源分配和管理。
- 线程: 创建、销毁和切换线程的开销相对较小,因为它们共享进程的资源。
- 独立性:
- 进程: 一个进程的崩溃通常不会影响其他进程。
- 线程: 如果一个进程中的某个线程崩溃,可能会导致整个进程的崩溃,因为它们共享相同的内存空间。
- 地址空间:
- 进程: 每个进程拥有独立的地址空间。
- 线程: 同一个进程内的所有线程共享相同的地址空间。
特性 | 进程 (Process) | 线程 (Thread) |
---|---|---|
基本单位 | 资源分配 | CPU 调度 |
拥有资源 | 拥有独立的内存空间和系统资源 | 共享所属进程的内存空间和大部分资源,拥有独立的栈和程序计数器 |
上下文切换 | 开销较大,需要切换内存空间 | 开销较小,只需切换CPU状态 |
通信方式 | 需要使用IPC机制(管道、消息队列、共享内存等) | 直接共享内存,通信高效,但需注意同步 |
创建/销毁开销 | 较大 | 较小 |
独立性 | 崩溃通常不影响其他进程 | 某个线程崩溃可能导致整个进程崩溃 |
地址空间 | 独立 | 共享 |