HarmonyOS宠物邻里实战第11篇:前后端集成测试、寄养链路与MongoDB数据校验
HarmonyOS宠物邻里实战第11篇:前后端集成测试、寄养链路与MongoDB数据校验
摘要
HarmonyOS App 前端页面写完以后,真正决定项目能不能稳定交付的,往往是后端接口和业务链路。宠物邻里项目里,前端要调用登录注册、宠物档案、寄养需求、留言回复、寄养申请、状态流转、评价、通知和账号注销等接口。只靠手动点页面,很容易漏掉越权、状态回退、数据残留和通知不同步的问题。
本文基于宠物邻里项目的 integration.js,复盘一条完整的前后端集成测试链路:
- 如何自动启动 Express 测试服务;
- 如何注册多个测试账号并拿到 Bearer Token;
- 如何创建宠物档案和寄养需求;
- 如何验证发布者不能发顶层留言;
- 如何验证非发布者不能越权回复;
- 如何验证申请、接单、开始、完成、评价的状态机;
- 如何从
/bootstrap快照确认 HarmonyOS 前端可消费的数据; - 如何直接查询 MongoDB,确认废弃集合和脏数据没有残留;
- 最后给出接口测试和前端联调验收清单。
这篇文章关注的是工程闭环:前端不是只要“能请求成功”,而是要后端状态真实可信,前端刷新才有意义。
工程背景与源码定位
| 文件 | 作用 |
|---|---|
houduan/test/test/integration.js |
完整业务链路集成测试 |
houduan/test/routes/api.js |
Express API 路由 |
houduan/test/middleware/security.js |
频率限制和登录身份识别 |
houduan/test/db.js |
MongoDB 连接 |
houduan/test/package.json |
测试命令 |
MyApp/entry/src/main/ets/services/ApiClient.ets |
HarmonyOS HTTP 封装 |
MyApp/entry/src/main/ets/services/BackendService.ets |
ArkTS 业务接口语义层 |
MyApp/entry/src/main/ets/common/MockStore.ets |
前端本地快照刷新 |
后端测试命令如下:
cd D:\APP\chong_wu_guan_li\houduan\test
npm run check
npm run test:integration
package.json 中定义了两类检查:
{
"scripts": {
"check": "node --check app.js && node --check db.js && node --check seed.js && node --check middleware/security.js && node --check routes/api.js && node --check routes/admin.js && node --check test/integration.js",
"test:integration": "node ./test/integration.js"
}
}
check 负责语法级检查,test:integration 负责业务链路验证。两者都需要保留,前者能快速发现语法错误,后者能发现接口行为错误。

一、测试服务如何启动
集成测试没有依赖外部手动启动服务,而是在脚本里启动 Express:
const { spawn } = require('child_process');
const port = 3107;
const baseUrl = `http://127.0.0.1:${port}/api`;
const server = spawn(process.execPath, ['./bin/www'], {
cwd: __dirname + '/..',
env: {
...process.env,
PORT: String(port),
HOST: '127.0.0.1',
API_RATE_LIMIT: '1000',
LOGIN_RATE_LIMIT: '100',
REGISTER_RATE_LIMIT: '100'
},
stdio: 'ignore',
windowsHide: true
});
这里有几个关键点:
| 配置 | 目的 |
|---|---|
PORT=3107 |
避免和开发服务端口冲突 |
HOST=127.0.0.1 |
只监听本机 |
API_RATE_LIMIT=1000 |
避免测试过程被限流影响 |
windowsHide: true |
Windows 下不弹出额外窗口 |
测试服务启动后,需要等待它真正可用:
async function waitForServer() {
for (let i = 0; i < 40; i++) {
try {
const response = await api('/health');
if (response.status === 200) {
return;
}
} catch (_error) {
// The child process may still be connecting to MongoDB.
}
await sleep(250);
}
throw new Error('Integration server did not become ready');
}
不要用固定 sleep(3000) 替代健康检查。MongoDB 连接、端口占用、服务启动失败,都应该被明确发现。
二、封装 api() 请求函数
测试脚本里封装了一个通用请求函数:
async function api(path, method = 'GET', token = '', body) {
const response = await fetch(baseUrl + path, {
method,
headers: {
'content-type': 'application/json',
...(token ? { authorization: `Bearer ${token}` } : {})
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const json = await response.json();
return { status: response.status, json };
}
这个函数和 HarmonyOS 端的 ApiClient 设计是对应的:
- 自动拼接
baseUrl; - 自动加
content-type; - 有 Token 时加
Authorization: Bearer; - 请求体统一 JSON 序列化;
- 返回 HTTP 状态码和业务 JSON。
前后端联调时,测试脚本和 ArkTS 客户端都应该使用同样的协议约定。否则测试过了,App 仍可能因为 Header、Token 或返回结构不一致而失败。
三、测试账号要自动创建和清理
集成测试使用时间后缀生成账号:
const password = 'TestPass123';
const suffix = String(Date.now()).slice(-8);
const createdAccounts = [];
async function register(prefix) {
const username = prefix + suffix;
const response = await api('/auth/register', 'POST', '', { username, password });
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
createdAccounts.push({
username,
token: response.json.data.token
});
return response.json.data;
}
测试里创建了三个角色:
| 变量 | 角色 |
|---|---|
owner |
宠物主人、寄养需求发布者 |
asker |
申请寄养的人 |
third |
第三方用户,用于验证越权 |
测试结束后清理账号:
async function cleanupAccounts() {
for (const account of createdAccounts) {
try {
await api('/auth/delete-account', 'POST', account.token, {
password,
confirmation: '注销账号'
});
} catch (_error) {
// The assertions should remain the primary failure.
}
}
}
这个清理动作很重要。集成测试如果只创建不清理,MongoDB 里会积累测试用户、宠物、帖子、申请和通知,后续测试会越来越不稳定。
四、先创建宠物,再创建寄养需求
寄养需求必须绑定宠物,所以测试先创建宠物档案:
const petId = `it_pet_${suffix}`;
let response = await api('/pets', 'POST', owner.token, {
id: petId,
name: '集成测试宠物',
species: '犬',
gender: 'male',
ageDesc: '2岁'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
然后创建寄养需求:
const requestId = `it_request_${suffix}`;
response = await api('/foster-requests', 'POST', owner.token, {
id: requestId,
title: '完整寄养链路测试',
petId,
fosterType: '家庭寄养',
startDate: '2026-07-01',
endDate: '2026-07-05',
location: '上海市 徐汇区',
latitude: 31.1884,
longitude: 121.4368,
budget: '100元/天',
feedRequirement: '每日两次',
walkRequirement: '每日两次',
notesRequirement: '公开留言沟通'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
assert.strictEqual(response.json.data.latitude, 31.1884);
assert.strictEqual(response.json.data.longitude, 121.4368);
这里不仅验证请求成功,还验证经纬度被正确保存。因为前端 FosterTab 的地图 Marker 依赖经纬度,如果后端漏存,HarmonyOS 地图页就只能靠地址猜坐标。
五、留言权限验证
寄养需求发布者不能给自己的需求发顶层留言:
response = await api(`/foster-requests/${requestId}/messages`, 'POST', owner.token, {
content: '发布者不能发顶层留言'
});
assert.strictEqual(response.status, 403);
申请者可以发留言:
const messageId = `it_message_${suffix}`;
response = await api(`/foster-requests/${requestId}/messages`, 'POST', asker.token, {
id: messageId,
content: '请问接送时间如何安排?'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
第三方不能越权回复:
response = await api(`/foster-messages/${messageId}/reply`, 'POST', third.token, {
content: '越权回复'
});
assert.strictEqual(response.status, 403);
发布者可以回复:
response = await api(`/foster-messages/${messageId}/reply`, 'POST', owner.token, {
content: '7月1日上午十点接送。'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
这一组断言覆盖了真实业务权限:谁能留言、谁能回复、谁不能越权。前端页面按钮可以隐藏,但后端权限必须自己兜住。
六、申请和状态流转
申请者提交寄养申请:
const applicationId = `it_application_${suffix}`;
response = await api(`/foster-requests/${requestId}/applications`, 'POST', asker.token, {
id: applicationId,
message: '我可以按要求照顾。'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
发布者接受申请:
response = await api(`/foster-applications/${applicationId}/status`, 'PUT', owner.token, {
status: 'accepted'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
接下来验证状态机不能乱跳:
const recordId = `rec_${requestId}`;
response = await api(`/foster-records/${recordId}/status`, 'PUT', owner.token, {
status: 'done'
});
assert.strictEqual(response.status, 409);
发布者不能直接把记录改成完成,因为流程还没有开始。申请者先确认开始:
response = await api(`/foster-records/${recordId}/status`, 'PUT', asker.token, {
status: 'ongoing'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
状态不能回退:
response = await api(`/foster-records/${recordId}/status`, 'PUT', asker.token, {
status: 'waitingIn'
});
assert.strictEqual(response.status, 409);
最后发布者确认完成:
response = await api(`/foster-records/${recordId}/status`, 'PUT', owner.token, {
status: 'done'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
这里测试了两个关键约束:
- 状态不能跳过必要步骤;
- 状态不能从后面的阶段回退到前面的阶段。
这类约束如果不写集成测试,很容易在前端调试时被误判成“页面状态没有刷新”。
七、评价和快照验证
完成后,宠物主人提交评价:
response = await api(`/foster-records/${recordId}/reviews`, 'POST', owner.token, {
rating: 5,
ratingTags: ['沟通顺畅', '信息透明'],
content: '完整链路验证通过。'
});
assert.strictEqual(response.status, 200, JSON.stringify(response.json));
然后通过 /bootstrap 读取前端启动快照:
const ownerSnapshot = await api('/bootstrap', 'GET', owner.token);
const askerSnapshot = await api('/bootstrap', 'GET', asker.token);
const request = ownerSnapshot.json.data.fosterRequests.find((item) => item.id === requestId);
const record = ownerSnapshot.json.data.fosterRecords.find((item) => item.id === recordId);
const storedMessage = askerSnapshot.json.data.fosterMessages.find((item) => item.id === messageId);
const ownerNotice = ownerSnapshot.json.data.notices.find((item) => item.messageId === messageId);
const askerNotice = askerSnapshot.json.data.notices.find((item) => item.messageId === messageId);
断言结果:
assert.strictEqual(request.status, 'done');
assert.strictEqual(record.status, 'done');
assert.ok(record.timeline.some((item) => item.text === '寄养开始'));
assert.ok(record.timeline.some((item) => item.text === '寄养完成'));
assert.ok(storedMessage.reply);
assert.strictEqual(ownerNotice.kind, 'system');
assert.strictEqual(askerNotice.kind, 'system');
这一步对 HarmonyOS 前端非常关键。因为前端 EntryAbility 和 BackendService.loadSnapshot() 需要消费的就是类似 /bootstrap 的快照数据。如果快照里没有 timeline、notice、reply,前端页面写得再好也显示不出来。
八、直接检查 MongoDB
测试最后直接连接 MongoDB:
const client = new MongoClient(process.env.MONGODB_URL || 'mongodb://127.0.0.1:27017');
await client.connect();
const db = client.db(process.env.MONGODB_DB || 'chongwu');
const collections = await db.listCollections({}, { nameOnly: true }).toArray();
assert.strictEqual(collections.some((item) => item.name === 'conversations'), false);
await client.close();
这条断言看起来特别小,但它有工程意义:确认旧的 conversations 集合没有被误创建。项目演进时,接口字段和集合名可能变过,如果旧集合被悄悄写入,后续迁移和清理都会变麻烦。
集成测试不仅要验证“有数据”,也要验证“不应该出现的数据没有出现”。
九、后端输入校验
api.js 里有一组输入清洗函数:
function text(value, maxLength) {
const result = String(value || '').trim();
if (result.length > maxLength || /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(result)) {
return null;
}
return result;
}
function textArray(value, maxItems = 10, maxLength = 30) {
if (!Array.isArray(value) || value.length > maxItems) {
return [];
}
return value
.map((item) => text(item, maxLength))
.filter((item) => item !== null && item.length > 0);
}
宠物输入也做了字段限制:
function petInput(body) {
return {
name: text(body.name, 30),
species: text(body.species, 30),
gender: body.gender === 'female' ? 'female' : 'male',
ageDesc: text(body.ageDesc, 30) || '',
vaccineRecord: text(body.vaccineRecord, 100) || '',
dewormRecord: text(body.dewormRecord, 100) || '',
dietHabit: text(body.dietHabit, 500) || '',
notes: text(body.notes, 1000) || '',
personalityTags: textArray(body.personalityTags, 12, 20)
};
}
这和前端表单校验是互补关系。HarmonyOS 页面可以提前限制输入,但后端必须再次校验,不能相信客户端。
十、账号和 Token 安全
后端使用 PBKDF2 处理密码:
function hashPassword(password, salt) {
return crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256').toString('hex');
}
function passwordMatches(password, account) {
const actual = Buffer.from(hashPassword(password, account.salt), 'hex');
const expected = Buffer.from(account.passwordHash, 'hex');
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
}
登录成功后创建 Session:
async function createSession(db, username, profileId) {
await db.collection('sessions').deleteMany({ profileId });
const token = crypto.randomBytes(32).toString('base64url');
const session = {
id: `session_${crypto.randomBytes(12).toString('hex')}`,
tokenHash: hashToken(token),
username,
profileId,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
};
await db.collection('sessions').insertOne(session);
return token;
}
这里有两个安全点:
- 数据库存 Token Hash,不存明文 Token;
- 同一用户登录时清理旧 Session,减少多端失控。
HarmonyOS 前端只拿到明文 Token,用于 Authorization: Bearer 请求头;后端只用 Hash 匹配。
十一、前端联调关注点
后端集成测试通过后,HarmonyOS 前端联调要重点看这些页面:
| 页面 | 验证点 |
|---|---|
| 登录页 | 注册、登录、错误密码、重复账号 |
| 宠物档案页 | 创建宠物后是否出现在 MockStore.pets |
| 寄养页 | 需求是否带经纬度,地图 Marker 是否出现 |
| 寄养详情页 | 留言、回复、申请按钮是否按角色显示 |
| 通知页 | 留言和状态流转是否生成系统通知 |
| 寄养中心 | 申请、邀请、记录是否按用户角色过滤 |
| 评价页 | 完成后是否能评价,评价后是否进入列表 |
联调时不要只看接口返回 200。要看 /bootstrap 快照是否能刷新到前端状态层,再看页面是否根据版本号更新。
十二、测试失败排查表
| 失败现象 | 可能原因 | 排查方式 |
|---|---|---|
Integration server did not become ready |
服务未启动或 MongoDB 未连接 | 检查端口、数据库地址、/health |
| 注册返回 429 | 限流配置太低 | 检查 REGISTER_RATE_LIMIT |
| 创建宠物 401 | Token 没传或过期 | 检查 authorization Header |
| 留言权限断言失败 | 角色判断错误 | 检查需求 authorId 和当前 profileId |
| 状态 409 没触发 | 状态机校验缺失 | 检查记录状态流转规则 |
/bootstrap 找不到记录 |
写入成功但快照映射漏字段 | 检查 map 和集合查询 |
| 通知不存在 | 状态流转没有生成 notice | 检查通知插入逻辑 |
| 清理失败 | delete-account 没覆盖关联集合 | 检查 deleteAccountData() |
这张表可以直接作为后续维护接口时的回归检查参考。
十三、验收清单
发布前我会按下面清单验收:
npm run check无语法错误;npm run test:integration能自动启动服务;- 测试账号使用时间后缀,避免冲突;
- 测试结束后调用账号注销清理数据;
- 宠物创建、寄养需求创建、经纬度保存通过;
- 留言和回复权限返回正确状态码;
- 申请接受后自动生成寄养记录;
- 状态机不能跳步,也不能回退;
- 完成后可以提交评价;
/bootstrap快照能看到需求、记录、留言、通知;- MongoDB 中不出现废弃集合;
- HarmonyOS 前端通过
BackendService能消费同样的数据结构。
总结
宠物邻里项目的集成测试不是为了追求测试覆盖率数字,而是为了保证 HarmonyOS 前端真正依赖的业务链路可靠。注册登录、宠物档案、寄养需求、留言回复、申请流转、状态时间线、评价和通知生成,这些环节只要有一个断掉,前端都会表现成“页面没数据”或“状态没刷新”。
通过 integration.js 自动启动服务、创建测试账号、执行完整寄养链路、读取 /bootstrap 快照、直连 MongoDB 校验集合状态,项目就有了一条可重复的后端验收线。前端继续迭代时,只要这条线稳定,ArkTS 页面和 MockStore 状态层就有可信的数据基础。
前后端项目越到后期,越需要这种不依赖手动点击的回归测试。它不显眼,但能帮项目少踩很多坑。
更多推荐

所有评论(0)