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));

这里测试了两个关键约束:

  1. 状态不能跳过必要步骤;
  2. 状态不能从后面的阶段回退到前面的阶段。

这类约束如果不写集成测试,很容易在前端调试时被误判成“页面状态没有刷新”。

七、评价和快照验证

完成后,宠物主人提交评价:

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 前端非常关键。因为前端 EntryAbilityBackendService.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 状态层就有可信的数据基础。

前后端项目越到后期,越需要这种不依赖手动点击的回归测试。它不显眼,但能帮项目少踩很多坑。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐