HarmonyOS开发:网络测试——网络层测试
HarmonyOS开发:网络测试——网络层测试
📌 核心要点:网络是移动端最不稳定的依赖——超时、断网、服务器500、DNS解析失败……网络层测试不是验证"正常请求能不能跑通",而是验证"各种异常情况下应用会不会崩、数据会不会丢、用户体验会不会炸"。
一、背景与动机
你在办公室WiFi下测得好好的,用户在地铁上网络抖了一下——请求超时,页面白屏,数据没了。你跟用户说"网络不好",用户跟你说"别的App怎么没这问题"?
网络层是应用中最脆弱的环节,没有之一。正常情况下一切OK,但网络一旦出问题,如果你的代码没处理好,后果就是:请求卡死(没超时)、数据丢失(失败没重试)、状态混乱(重试导致重复提交)、体验崩溃(白屏无反馈)。
手动测试网络异常?你断一下WiFi试试?那只能测一种场景。超时呢?DNS失败呢?服务器返回500呢?SSL证书错误呢?这些场景你手动模拟不了,或者说模拟的成本太高。网络层测试就是用代码系统性地覆盖这些异常场景,确保你的应用在网络不好的时候也能优雅降级,而不是直接崩掉。
二、核心原理
2.1 网络测试场景矩阵
flowchart TB
A[网络测试场景] --> B[正常场景]
A --> C[超时场景]
A --> D[离线场景]
A --> E[错误场景]
B --> B1[GET请求正常响应]
B --> B2[POST请求正常响应]
B --> B3[大文件下载]
B --> B4[并发请求]
C --> C1[连接超时]
C --> C2[读取超时]
C --> C3[重试机制验证]
C --> C4[超时后状态恢复]
D --> D1[完全离线]
D --> D2[弱网环境]
D --> D3[网络恢复后自动重连]
D --> D4[离线数据缓存]
E --> E1[HTTP 4xx错误]
E --> E2[HTTP 5xx错误]
E --> E3[DNS解析失败]
E --> E4[SSL证书错误]
E --> E5[响应体格式错误]
classDef mainStyle fill:#4CAF50,stroke:#388E3C,color:#fff,font-weight:bold
classDef normalStyle fill:#3498DB,stroke:#2980B9,color:#fff
classDef timeoutStyle fill:#F44336,stroke:#D32F2F,color:#fff
classDef offlineStyle fill:#F39C12,stroke:#E67E22,color:#fff
classDef errorStyle fill:#9B59B6,stroke:#8E44AD,color:#fff
class A mainStyle
class B,B1,B2,B3,B4 normalStyle
class C,C1,C2,C3,C4 timeoutStyle
class D,D1,D2,D3,D4 offlineStyle
class E,E1,E2,E3,E4,E5 errorStyle
2.2 HarmonyOS网络请求核心机制
HarmonyOS通过@ohos.net.http模块处理HTTP请求,通过@ohos.net.connection模块检测网络状态:
import http from '@ohos.net.http'
import connection from '@ohos.net.connection'
// 发起HTTP请求
const httpRequest = http.createHttp()
const response = await httpRequest.request(url, {
method: http.RequestMethod.GET,
connectTimeout: 10000, // 连接超时10秒
readTimeout: 30000 // 读取超时30秒
})
// 检测网络状态
const netHandle = connection.getDefaultNet()
const netConnection = connection.createNetConnection(netHandle)
netConnection.on('netAvailable', () => { /* 网络可用 */ })
netConnection.on('netLost', () => { /* 网络断开 */ })
2.3 网络错误分类与处理策略
| 错误类型 | HTTP状态码 | 处理策略 | 用户反馈 |
|---|---|---|---|
| 客户端错误 | 400/401/403/404 | 不重试,提示具体原因 | “资源不存在”/“无权限” |
| 服务端错误 | 500/502/503 | 指数退避重试 | “服务器开小差了” |
| 连接超时 | 无状态码 | 重试2-3次 | “网络超时,点击重试” |
| DNS失败 | 无状态码 | 不重试(DNS不会突然好) | “网络连接失败” |
| 网络断开 | 无状态码 | 监听网络恢复后自动重试 | “网络不可用” |
| 响应格式错误 | 200但解析失败 | 不重试,上报异常 | “数据加载异常” |
三、代码实战
3.1 基础用法:网络请求层封装与测试
// NetworkClient.ets - 网络请求客户端
import http from '@ohos.net.http'
export interface RequestConfig {
url: string
method?: http.RequestMethod
headers?: Record<string, string>
body?: string
connectTimeout?: number
readTimeout?: number
retryCount?: number // 重试次数
retryDelay?: number // 重试间隔(毫秒)
}
export interface NetworkResponse<T> {
success: boolean
statusCode: number
data: T | null
error: string
retryCount: number // 实际重试次数
duration: number // 请求耗时
}
export class NetworkClient {
private defaultRetryCount: number = 2
private defaultRetryDelay: number = 1000
private defaultConnectTimeout: number = 10000
private defaultReadTimeout: number = 30000
// 发起请求(带重试机制)
async request<T>(config: RequestConfig): Promise<NetworkResponse<T>> {
const maxRetry = config.retryCount ?? this.defaultRetryCount
const retryDelay = config.retryDelay ?? this.defaultRetryDelay
let lastError: string = ''
let retryCount = 0
const startTime = Date.now()
for (let attempt = 0; attempt <= maxRetry; attempt++) {
try {
const result = await this.doRequest<T>(config)
result.retryCount = retryCount
result.duration = Date.now() - startTime
return result
} catch (e) {
lastError = (e as Error).message
retryCount = attempt
// 判断是否应该重试
if (!this.shouldRetry(lastError, attempt, maxRetry)) {
break
}
// 等待后重试(指数退避)
if (attempt < maxRetry) {
const delay = retryDelay * Math.pow(2, attempt)
await new Promise<void>(resolve => setTimeout(resolve, delay))
}
}
}
return {
success: false,
statusCode: 0,
data: null,
error: lastError,
retryCount: retryCount,
duration: Date.now() - startTime
}
}
// 执行单次请求
private async doRequest<T>(config: RequestConfig): Promise<NetworkResponse<T>> {
const httpRequest = http.createHttp()
try {
const response = await httpRequest.request(config.url, {
method: config.method ?? http.RequestMethod.GET,
header: config.headers,
extraData: config.body,
connectTimeout: config.connectTimeout ?? this.defaultConnectTimeout,
readTimeout: config.readTimeout ?? this.defaultReadTimeout
})
if (response.responseCode >= 200 && response.responseCode < 300) {
let data: T | null = null
if (response.result) {
data = typeof response.result === 'string'
? JSON.parse(response.result) as T
: response.result as T
}
return { success: true, statusCode: response.responseCode, data, error: '', retryCount: 0, duration: 0 }
}
return {
success: false,
statusCode: response.responseCode,
data: null,
error: `HTTP ${response.responseCode}`,
retryCount: 0,
duration: 0
}
} finally {
httpRequest.destroy()
}
}
// 判断是否应该重试
private shouldRetry(error: string, attempt: number, maxRetry: number): boolean {
if (attempt >= maxRetry) return false
// 服务端错误和超时可以重试
if (error.includes('500') || error.includes('502') || error.includes('503')) return true
if (error.includes('超时') || error.includes('timeout')) return true
// DNS错误、4xx错误不重试
return false
}
}
3.2 进阶用法:网络错误模拟与超时测试
// NetworkErrorSimulator.ets - 网络错误模拟器
export class NetworkErrorSimulator {
private errorMode: 'normal' | 'timeout' | 'offline' | 'server_error' | 'dns_error' | 'malformed' = 'normal'
private errorConfig: {
timeoutMs?: number
statusCode?: number
responseBody?: string
} = {}
// 设置错误模式
setErrorMode(mode: 'normal' | 'timeout' | 'offline' | 'server_error' | 'dns_error' | 'malformed', config?: {
timeoutMs?: number
statusCode?: number
responseBody?: string
}): void {
this.errorMode = mode
this.errorConfig = config ?? {}
}
// 恢复正常模式
resetToNormal(): void {
this.errorMode = 'normal'
this.errorConfig = {}
}
// 模拟请求(替代真实HTTP请求)
async simulateRequest(url: string, method: string): Promise<SimulatedResponse> {
switch (this.errorMode) {
case 'timeout':
// 模拟超时:等待很长时间
await new Promise<void>(resolve => setTimeout(resolve, this.errorConfig.timeoutMs ?? 60000))
return { statusCode: 0, body: '', error: '连接超时' }
case 'offline':
return { statusCode: 0, body: '', error: '网络不可用' }
case 'server_error':
return {
statusCode: this.errorConfig.statusCode ?? 500,
body: this.errorConfig.responseBody ?? '{"error":"Internal Server Error"}',
error: ''
}
case 'dns_error':
return { statusCode: 0, body: '', error: 'DNS解析失败' }
case 'malformed':
return {
statusCode: 200,
body: this.errorConfig.responseBody ?? '这不是合法的JSON{{{',
error: ''
}
case 'normal':
default:
return { statusCode: 200, body: '{"success":true,"data":{}}', error: '' }
}
}
}
export interface SimulatedResponse {
statusCode: number
body: string
error: string
}
// ==================== 网络错误测试 ====================
import { describe, it, beforeAll, beforeEach, assertEqual, assertTrue, assertNotNull } from '@ohos/hypium'
import { NetworkClient, RequestConfig } from '../NetworkClient'
import { NetworkErrorSimulator } from '../NetworkErrorSimulator'
export default function networkErrorTest() {
describe('网络错误场景测试', () => {
let client: NetworkClient
let simulator: NetworkErrorSimulator
beforeAll(() => {
client = new NetworkClient()
simulator = new NetworkErrorSimulator()
})
beforeEach(() => {
simulator.resetToNormal()
})
it('正常请求_成功返回数据', 0, async () => {
const response = await client.request({
url: 'https://httpbin.org/get',
method: http.RequestMethod.GET,
retryCount: 0
})
assertTrue(response.success, '正常请求应成功')
assertEqual(response.statusCode, 200)
})
it('连接超时_返回错误信息', 0, async () => {
const response = await client.request({
url: 'https://httpbin.org/delay/10', // 服务端延迟10秒
method: http.RequestMethod.GET,
connectTimeout: 2000, // 2秒连接超时
retryCount: 0
})
assertTrue(!response.success, '超时请求应失败')
assertTrue(response.error.includes('超时') || response.error.includes('timeout'),
'错误信息应包含超时提示')
})
it('服务端500_自动重试', 0, async () => {
const response = await client.request({
url: 'https://httpbin.org/status/500',
method: http.RequestMethod.GET,
retryCount: 2,
retryDelay: 100
})
assertTrue(!response.success, '500请求应失败')
assertTrue(response.retryCount > 0, '应该有重试')
assertEqual(response.statusCode, 500)
})
it('404错误_不重试', 0, async () => {
const response = await client.request({
url: 'https://httpbin.org/status/404',
method: http.RequestMethod.GET,
retryCount: 2,
retryDelay: 100
})
assertTrue(!response.success, '404请求应失败')
assertEqual(response.retryCount, 0, '404不应重试')
assertEqual(response.statusCode, 404)
})
it('DNS解析失败_不重试', 0, async () => {
const response = await client.request({
url: 'https://this-domain-does-not-exist-12345.com/api',
method: http.RequestMethod.GET,
retryCount: 2,
retryDelay: 100
})
assertTrue(!response.success, 'DNS失败应返回错误')
assertEqual(response.retryCount, 0, 'DNS失败不应重试')
})
it('重试耗时应累加', 0, async () => {
const response = await client.request({
url: 'https://httpbin.org/status/503',
method: http.RequestMethod.GET,
retryCount: 2,
retryDelay: 200
})
// 2次重试,每次间隔200ms起(指数退避),总耗时至少400ms
assertTrue(response.duration >= 200, `重试耗时应累加,实际${response.duration}ms`)
})
})
}
3.3 完整示例:离线场景与网络恢复测试
// OfflineManager.ets - 离线管理器
import connection from '@ohos.net.connection'
export interface PendingRequest {
id: string
url: string
method: string
body: string
createdAt: number
retryCount: number
}
export class OfflineManager {
private pendingRequests: Array<PendingRequest> = []
private isOnline: boolean = true
private netConnection: connection.NetConnection | null = null
private onNetworkRestored: ((pendingRequests: Array<PendingRequest>) => void) | null = null
// 初始化网络监听
async init(): Promise<void> {
this.netConnection = connection.createNetConnection()
this.netConnection.on('netAvailable', () => {
this.isOnline = true
this.onNetworkRestored?.(this.pendingRequests)
})
this.netConnection.on('netLost', () => {
this.isOnline = false
})
}
// 设置网络恢复回调
setOnNetworkRestored(callback: (pendingRequests: Array<PendingRequest>) => void): void {
this.onNetworkRestored = callback
}
// 添加待处理请求
addPendingRequest(request: PendingRequest): void {
this.pendingRequests.push(request)
}
// 获取所有待处理请求
getPendingRequests(): Array<PendingRequest> {
return [...this.pendingRequests]
}
// 移除已处理的请求
removePendingRequest(id: string): void {
this.pendingRequests = this.pendingRequests.filter(r => r.id !== id)
}
// 清空所有待处理请求
clearPendingRequests(): void {
this.pendingRequests = []
}
// 获取在线状态
getIsOnline(): boolean {
return this.isOnline
}
// 模拟设置在线状态(测试用)
setOnlineStatus(online: boolean): void {
this.isOnline = online
}
// 销毁
destroy(): void {
this.netConnection?.unregister('netAvailable')
this.netConnection?.unregister('netLost')
}
}
// ==================== 离线场景测试 ====================
import { describe, it, beforeAll, afterAll, beforeEach, assertEqual, assertTrue } from '@ohos/hypium'
import { OfflineManager, PendingRequest } from '../OfflineManager'
import { NetworkClient } from '../NetworkClient'
export default function offlineScenarioTest() {
describe('离线场景测试', () => {
let offlineManager: OfflineManager
let networkClient: NetworkClient
beforeAll(() => {
offlineManager = new OfflineManager()
networkClient = new NetworkClient()
})
afterAll(() => {
offlineManager.destroy()
})
beforeEach(() => {
offlineManager.clearPendingRequests()
offlineManager.setOnlineStatus(true)
})
it('离线时请求失败_加入待处理队列', 0, async () => {
offlineManager.setOnlineStatus(false)
// 模拟离线时的请求
const pendingReq: PendingRequest = {
id: 'req-001',
url: 'https://api.example.com/orders',
method: 'POST',
body: JSON.stringify({ productId: 'p-001', quantity: 2 }),
createdAt: Date.now(),
retryCount: 0
}
offlineManager.addPendingRequest(pendingReq)
// 验证待处理队列
const pending = offlineManager.getPendingRequests()
assertEqual(pending.length, 1, '应有1个待处理请求')
assertEqual(pending[0].id, 'req-001')
})
it('网络恢复后_自动重发待处理请求', 0, async () => {
let restoredCallbackFired = false
// 添加待处理请求
offlineManager.setOnlineStatus(false)
offlineManager.addPendingRequest({
id: 'req-001',
url: 'https://api.example.com/orders',
method: 'POST',
body: JSON.stringify({ productId: 'p-001', quantity: 2 }),
createdAt: Date.now(),
retryCount: 0
})
// 设置网络恢复回调
offlineManager.setOnNetworkRestored((pendingRequests) => {
restoredCallbackFired = true
assertEqual(pendingRequests.length, 1, '回调应传入待处理请求')
})
// 模拟网络恢复
offlineManager.setOnlineStatus(true)
// 手动触发回调(实际由netAvailable事件触发)
const pending = offlineManager.getPendingRequests()
offlineManager.setOnlineStatus(true)
assertTrue(restoredCallbackFired, '网络恢复回调应被触发')
})
it('多次离线请求_队列按顺序保存', 0, async () => {
offlineManager.setOnlineStatus(false)
for (let i = 1; i <= 5; i++) {
offlineManager.addPendingRequest({
id: `req-${i.toString().padStart(3, '0')}`,
url: 'https://api.example.com/orders',
method: 'POST',
body: JSON.stringify({ index: i }),
createdAt: Date.now() + i,
retryCount: 0
})
}
const pending = offlineManager.getPendingRequests()
assertEqual(pending.length, 5, '应有5个待处理请求')
assertEqual(pending[0].id, 'req-001', '应按添加顺序排列')
assertEqual(pending[4].id, 'req-005')
})
it('请求成功后_从待处理队列移除', 0, async () => {
offlineManager.addPendingRequest({
id: 'req-001',
url: 'https://api.example.com/orders',
method: 'POST',
body: '{}',
createdAt: Date.now(),
retryCount: 0
})
// 模拟请求成功
offlineManager.removePendingRequest('req-001')
const pending = offlineManager.getPendingRequests()
assertEqual(pending.length, 0, '成功后应从队列移除')
})
it('弱网场景_请求超时但不崩溃', 0, async () => {
// 设置极短超时模拟弱网
const response = await networkClient.request({
url: 'https://httpbin.org/delay/5', // 服务端延迟5秒
method: http.RequestMethod.GET,
connectTimeout: 1000, // 1秒连接超时
readTimeout: 1000, // 1秒读取超时
retryCount: 1,
retryDelay: 100
})
// 关键验证:即使请求失败,也不应崩溃
assertTrue(!response.success, '弱网请求应失败')
assertNotNull(response.error, '应有错误信息')
assertTrue(response.duration > 0, '应记录耗时')
})
})
}
四、踩坑与注意事项
坑点1:超时时间设置不合理
连接超时设1秒?在弱网环境下,1秒根本连不上,正常请求也会超时。读取超时设60秒?用户等一分钟才看到"加载失败",体验极差。
建议:连接超时5-10秒,读取超时15-30秒。根据业务场景调整——实时性要求高的接口(如搜索)超时短一些,文件上传/下载超时长一些。
坑点2:重试导致重复操作
用户点"提交订单",网络超时了,你自动重试了——结果订单创建了两次。这不是重试的错,是你没做幂等处理。
解决方案:写操作(POST/PUT/DELETE)重试时带上幂等Token,服务端根据Token去重。或者干脆写操作不自动重试,让用户手动重试。
坑点3:网络状态监听不靠谱
connection.on('netAvailable')和connection.on('netLost')在某些设备上不可靠——WiFi切换到4G时可能两个事件都不触发。如果你的离线逻辑依赖这些事件,可能会出现"网络已经恢复了但App还以为离线"的情况。
建议:不要完全依赖网络状态事件。在发起请求前主动检测网络状态(connection.getDefaultNet()),请求失败后也主动检测,而不是只依赖事件。
坑点4:测试中直接请求外部服务器
测试代码直接请求httpbin.org或真实API服务器——网络一抖测试就挂了。CI环境可能根本无法访问外网。
建议:本地搭建Mock Server或使用网络错误模拟器。只有在对真实服务做集成测试时才请求外部服务器,而且要加@Flaky标记允许偶发失败。
坑点5:HTTP资源泄漏
http.createHttp()创建的请求对象必须destroy()。在网络错误场景中,如果请求抛了异常但没走到destroy(),资源就泄漏了。
建议:用try-finally确保资源释放,这是铁律。
坑点6:并发请求的竞态条件
用户快速点击"刷新"按钮,触发了3个并发请求。第1个请求还在处理中,第3个请求的结果先回来了——UI显示的是第3个请求的数据,然后第1个请求的结果又回来了,覆盖了第3个的结果。
解决方案:给每个请求加版本号,只处理最新版本的响应。或者在前一个请求完成前取消后续请求(httpRequest.destroy()可以取消正在进行的请求)。
坑点7:SSL证书问题在测试环境
测试环境用自签名证书,http.request默认会校验SSL证书,直接报错。你在代码里关了SSL校验,忘了在上线前打开——安全漏洞。
建议:通过环境变量控制SSL校验开关,生产环境强制开启。不要在代码里硬编码rejectUnauthorized: false。
五、HarmonyOS 6适配说明
API差异表
| 功能/接口 | HarmonyOS 5 | HarmonyOS 6 | 变更说明 |
|---|---|---|---|
| HTTP请求 | @ohos.net.http | @ohos.net.http | 新增请求拦截器 |
| 网络状态 | @ohos.net.connection | @ohos.net.connection | 新增网络质量评估 |
| 网络模拟 | 需自实现 | @ohos.net.networkSimulator | 官方网络模拟器 |
| 离线缓存 | 需自实现 | @ohos.net.offlineCache | 官方离线缓存 |
| 请求重试 | 需自实现 | RetryPolicy | 内置重试策略 |
行为变更
-
请求拦截器:HarmonyOS 6支持请求/响应拦截器,可以在请求发出前统一添加Token、日志,在响应返回后统一处理错误码。
-
网络质量评估:
connection.getConnectionQuality()返回当前网络质量(好/中/差),可以根据质量动态调整请求策略(如弱网时降低图片分辨率)。 -
官方网络模拟器:
@ohos.net.networkSimulator可以在测试中模拟各种网络状况(超时、断网、弱网、高延迟),不需要自己实现模拟器。
适配代码
// HarmonyOS 6网络测试
import { describe, it, assertEqual, assertTrue } from '@ohos/hypium'
import { NetworkSimulator, NetworkCondition } from '@ohos.net.networkSimulator'
import { RetryPolicy, ExponentialBackoff } from '@ohos.net.http'
export default function harmonyOS6NetworkTest() {
describe('HarmonyOS 6网络测试', () => {
it('网络模拟器_模拟超时', 0, async () => {
const simulator = new NetworkSimulator()
// 设置超时条件
simulator.setCondition(NetworkCondition.TIMEOUT, { timeoutMs: 1000 })
const response = await client.request({ url: '/api/test' })
assertTrue(!response.success, '超时条件下请求应失败')
simulator.reset()
})
it('网络模拟器_模拟弱网', 0, async () => {
const simulator = new NetworkSimulator()
// 设置弱网条件:延迟500ms,丢包率20%
simulator.setCondition(NetworkCondition.POOR, { latency: 500, packetLoss: 0.2 })
const response = await client.request({ url: '/api/test' })
// 弱网下请求可能成功也可能失败,但不应该崩溃
assertNotNull(response)
simulator.reset()
})
it('内置重试策略_指数退避', 0, async () => {
const retryPolicy = new RetryPolicy()
.maxRetries(3)
.backoff(new ExponentialBackoff().initialDelay(100).maxDelay(5000))
.retryOn([500, 502, 503])
const response = await client.request({
url: '/api/test',
retryPolicy: retryPolicy
})
assertNotNull(response)
})
})
}
六、总结
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ |
| 使用频率 | ⭐⭐⭐⭐⭐ |
| 重要程度 | ⭐⭐⭐⭐⭐ |
网络测试的核心不是验证"正常请求能不能跑通"——那太简单了。核心是验证异常场景下的行为:超时了会不会卡死?断网了数据会不会丢?重试了会不会重复提交?500了用户看到什么?这些问题不测,上线就是定时炸弹。超时和重试是网络测试的重头戏,但一定要配合幂等设计,否则重试就是灾难。离线场景经常被忽视,但移动端用户最常遇到的就是网络不稳定——你的App在离线时的表现,决定了用户会不会卸载你。网络测试做得好不好,看的是"最差情况"而不是"最好情况"。
- 点赞
- 收藏
- 关注作者
评论(0)