2026秋软件工程结对作业(第二次之程序实现)|拾见校园的规则与测试

2026秋软件工程结对作业(第二次之程序实现)|拾见校园的规则与测试

项目 内容
课程 H202601 软件工程与软件工程实践
作业要求 软件工程结对作业第二次:程序实现
本作业博客 本篇作业博文
作业目标 基于前次失物招领原型完成可运行程序,说明设计和测试方法,并通过 GitHub 完成真实的结对协作
本人 黄锦培,102401221,GitHub:coderbrain404
结对同学 姚华俊,082403146;同学本次作业博文;GitHub
项目主仓库 Grim761/082403146-102401221
在线体验 拾见校园实现版(本机保存)
个人 fork / PR 个人 fork · 本次提交 4e9d0c2 · PR #1

一、这次实现关注的事情

“拾见校园”延续了前次的校园线索卡原型。失主可以发布寻物,拾获者可以发布招领,同学按名称、地点和时间核对信息,完成交接后由发布者更新状态。

我更关心这条流程里的规则是否一致。页面能显示“发布成功”,还需要确认数据真的保存了;页面有“已归还”,还需要确认其他浏览者看到的也是这个结果;自己的记录有修改按钮,还需要确认别人的记录不能被修改。只有把这些规则说清楚,后续测试才有确定的预期。

项目采用 HTML、CSS、JavaScript 和一个无第三方依赖的 Node 服务。页面可以作为静态程序运行,服务模式则补上不同账号间的共享数据。本文主要说明业务规则、持久保存与测试设计,页面和运行说明也一并列出。

校园线索墙

二、功能和状态约定

功能 当前实现
寻物 / 招领发布 校验必要字段,可添加描述与一张图片
浏览搜索 关键词与类型、类别、区域、状态共同筛选
详情联系 显示发生时间、地点、特征与领取信息,支持复制
状态更新 发布者标记已找到 / 已归还;确认后保存;可重新开放
我的发布 按当前身份查看自己的记录,筛选处理中 / 已完成
数据保存 本机保存到浏览器;共享服务保存到 JSON 文件
操作补充 文字草稿恢复、发布前卡片预览、图片压缩、本机备份导出

记录中 kind 区分 lost 和 found,status 区分 open 和 resolved。显示文字由这两个字段共同决定:

类型 open resolved
lost 寻找中 已找到
found 招领中 已归还

已完成线索默认不出现在首页列表中,勾选“包含已完成”可看历史;详情收起领取信息。这个操作不删除存储中的原联系方式,也不能撤回已经被复制的信息。

发布到完成的流程

三、结对工作与时间安排

1. 分工

开发计划中拟定姚华俊侧重页面、表单和完整交互流程,黄锦培侧重搜索与状态规则复核、边界测试、共享流程和文档复核。下表只列目前可从仓库提交和测试记录核对的工作;线下讨论及个人复核时长不凭仓库记录推断。

成员 实际工作记录
姚华俊 以 Grim761 账号在主仓库分阶段提交业务规则、共享服务、页面流程、文档和测试;具体线下分工以本人记录为准。
黄锦培 以 coderbrain404 账号在个人 fork 同步主仓库链接修正,补充服务端文件存储的两项回归测试,更新测试入口、README 和测试报告,并提交 PR #1;本地运行 43 项测试均通过。

本次修改已推送到个人 fork 的 codex/store-recovery-tests 分支,并创建了指向 Grim761:main 的 PR #1。PR 已于 2026-10-10 22:33(北京时间)合并到主仓库 main,合并提交为 669c4b8。

2. PSP

本轮工作预估已在编码前的共同开发计划中记录。下面沿用该计划,单位为分钟。实际列是根据完成的工作回顾估算的个人投入时间,不是计时器记录;AI 辅助生成代码和文档的运行时间不单独计入。父阶段为小计,不重复加总。

PSP2.1 阶段 预估耗时 本人耗时(回顾估算)
Planning 计划 15 10
· Estimate 任务拆分与时间估计 15 10
Development 开发 405 220
· Analysis 需求分析与技术学习 40 20
· Design Spec 设计文档 25 15
· Design Review 设计复审 15 10
· Coding Standard 代码规范 10 5
· Design 具体设计 35 15
· Coding 编码 180 60
· Code Review 代码复审 25 25
· Test 测试与修复 75 70
Reporting 报告 60 50
· Test Report 测试报告 25 25
· Size Measurement 工作量统计 10 5
· Postmortem & Process Improvement Plan 总结与改进计划 25 20
合计 480 280 分钟(约 4 小时 40 分)

与预估相比,编码时间缩短较多,主要因为借助 AI 辅助完成初稿,再把人工时间放在检查文件存储失败路径、运行测试和核对 PR 差异上。测试与复核没有按编码时间同比例缩短。由于未在每次切换任务时实时计时,上表只能作为回顾估算;下次应记录开始与结束时间,减少回忆误差。

四、数据与模块设计

1. 一条记录需要哪些信息

核心记录 Post 保存物品名称、类别、区域、具体地点、事件时间、描述、领取信息和可选图片。此外还有唯一 ID、发布者 ID 与署名、状态、创建和更新时间以及示例标记。

发生时间和发布时间分别保存。例如昨天遗失的物品今天才发布,搜索时仍应能看到昨天的事件时间。完成状态更新时间则用来说明线索何时处理结束。

2. 独立的业务规则

core.js 提供输入校验、筛选与状态变更等函数,不依赖页面元素。app.js 负责获取表单值和显示反馈,storage.js 负责浏览器存储。api.js 在连接到本项目服务时选择共享接口,其余情况下使用本机保存。

共享服务中,账号身份由会话确定,接口重复执行字段校验和发布者检查。store.js 用单进程事务队列管理 JSON 写入,保存完成后才提交新的内存快照。

模块与数据流程

3. 两种运行方式

本机模式适合下载后体验,身份和记录只保存在当前浏览器;不能当作多用户身份认证。共享模式需要启动 Node 服务,同一服务中的不同账号可以看到同一份记录。两个模式的数据相互独立,页面右上角显示当前模式。

这一点需要在运行说明中讲清楚,否则“刷新后保存”容易被误解为“换设备也能同步”。GitHub Pages 在线链接只提供静态本机体验,完整共享演示在本地服务中完成。

五、关键代码和额外功能

1. 搜索条件共同生效

搜索不仅检查标题,也检查描述、区域、具体地点和类别。用户输入多个关键词时,要共同匹配,避免一个过于宽泛的词把大量无关线索带进结果。

const words = text(filters.query).toLocaleLowerCase().split(/\s+/).filter(Boolean);
const haystack = [post.title, post.description, post.zone, post.place, post.category]
  .join(' ').toLocaleLowerCase();
return words.every(word => haystack.includes(word));

类别、区域、类型和状态还会逐项判断。例如指定“招领+图书馆+证件卡片+处理中”,结果必须同时满足四项。这是本次额外功能之一:同类物品较多时,先缩小区域再核对时间,可以少打开几张不相关卡片。

2. 发布者检查必须在规则和接口中落实

页面隐藏他人的更新按钮之外,状态函数仍然拒绝归属不符的操作:

if (!actorId || post.ownerId !== actorId) {
  throw new DomainError('只有发布者可以修改这条线索。', 'FORBIDDEN', 403);
}

共享接口从 Cookie 会话取得用户,传入该用户的 ID;不接受客户端指定一个任意发布者来取得权限。接口测试中使用第二个账号直接发更新请求,预期返回 403。

3. 文件保存避免两次修改相互覆盖

如果两个请求同时读取旧数据、分别修改后写回,就可能只留下最后一次。当前单进程存储把事务排队,每次在上一事务完成后从最新快照开始操作:

const task = queue.then(async () => {
  const next = JSON.parse(JSON.stringify(db));
  const result = await action(next);
  await write(next);
  db = next;
  return result;
});
queue = task.catch(() => {});
return task;

写入使用临时文件和重命名替换。如果事务失败,不提交新的快照;队列也不应因为一次失败而永久停止。并发集成测试检查两个独立修改同时提交后都被保留;本次补充的文件存储测试进一步检查损坏 JSON 不被覆盖,以及事务抛错后内存和磁盘不变、下一次事务仍能保存。文件存储的适用范围是一个服务进程,不支持多实例数据库式写入。

4. 联系信息一键复制

第二个额外功能是复制联系方式或领取地点。页面优先调用 navigator.clipboard.writeText,失败时尝试临时文本框,最终失败会提示手动复制。这样可以减少手机上长按选中文字的步骤。

await navigator.clipboard.writeText(post.contact);

这行代码在实现中放在成功 / 失败处理内,完成线索也不再出现复制按钮。实际浏览器检查已确认复制的文本与详情字段一致。

六、白盒测试怎样设计

1. 工具和运行方式

测试使用 Node 内置 node:test 与 node:assert/strict。项目没有第三方 npm 依赖,运行前不需要安装测试框架。

npm test
node --test --experimental-test-coverage tests/core.test.js tests/storage.test.js tests/store.test.js tests/server.test.js

core.test.js 测业务函数,storage.test.js 使用可控制的模拟存储测试成功和异常路径,store.test.js 测服务端文件保存的失败恢复,server.test.js 启动临时端口与数据目录测试接口。实际运行数据不会被这些测试覆盖。

2. 从判断分支选择数据

先看函数中有哪些分支,再为分支选择数据。标题上限是 40,就选择 40 和 41;发布者比较 ID,就选择本人、另一个人和空身份;保存可能抛错,就让模拟 setItem 抛出配额异常。不能只测试一条能成功发布的记录。

下面这项测试不只检查“发布报错”,还检查失败之后列表没有被改动:

test('存储配额不足:发布失败时列表保持不变', async () => {
  const { storage, store } = setup();
  const before = await store.list();
  storage.setItem = () => { throw new Error('QuotaExceededError'); };
  await assert.rejects(store.create(input), e => e.code === 'STORAGE_FULL');
  assert.deepEqual(await store.list(), before);
});

3. 代表性用例

用例 覆盖的判断 预期
必填字段逐项缺失 多个字段的错误分支 返回各自提示
标题 40 / 41 字 最大长度边界 接受 40、拒绝 41
空白与未来时间 trim 后长度、时间比较 拒绝空白内容和明显未来事件
多词、组合筛选 every 与条件返回 同时满足才进入结果
本人 / 他人更新 归属判断 本人成功,他人 403
存储失败与重载 commit 和初始化 失败不改列表,成功数据可恢复
重复完成 状态不变分支 不制造新更新时间
并发修改 事务队列 两个修改都保存
损坏文件 文件读取异常 拒绝启动且原始字节不变
事务失败后重试 队列与内存快照 失败不改磁盘,后续事务仍能保存

完整白盒用例列在附录,四个测试文件可在主仓库的测试目录查看;输入设计、运行命令和结果另见测试报告。

4. 当前测试结果

2026-10-10,合并后的版本在本地 Node v24.16.0 上测试结果为 43 项通过、0 项失败,包含 29 项单元测试和 14 项接口与集成测试。被 Node 测试加载的模块合计行覆盖率 94.72%、分支覆盖率 76.89%,没有把页面交互脚本算进这个数字。测试报告列出输入、断言、命令和结果。

Edge 与内置浏览器中已走查发布、草稿恢复、复制、图片刷新保存、两类完成状态和另一账号的入口权限。1440px 和 390px 两个断点没有出现横向溢出。本次另从个人 fork 下载完整 ZIP,解压后用 Chrome 的无界面模式直接打开 index.html,确认“本机保存”和线索墙;无结果搜索、发布进入详情、刷新后保留、本人标记已找到和已完成筛选均通过。查看本机模式截图。共享服务在两个独立 Chrome 配置中检查了 A 发布、B 查看、B 更新被拒绝(403),以及 A 更新后 B 刷新看到完成状态。Chrome 中的图片选择、剪贴板实际写入和日常操作体验仍需人工复核。

Chrome 中账号 B 查看账号 A 的线索

账号 A 更新后,Chrome 中账号 B 刷新看到已找到

PR 合并提交 669c4b8 的 GitHub Actions 测试与静态页面发布均已成功;本地 43 项测试结果和覆盖范围见上文。

七、目录、使用方式与界面

项目根目录/
├── index.html
├── css/style.css
├── js/           # 业务、示例、本机存储、接口、物品示意图、页面交互
├── server/       # HTTP 服务和文件存储
├── tests/        # 业务、本机存储、服务端文件存储、接口测试
├── docs/         # 计划、报告、图片
├── tools/        # 图示生成源文件
├── .github/      # 测试与静态部署流程
├── README.md
└── package.json

直接体验时,从 GitHub 下载完整 ZIP 并解压,用 Chrome 打开 index.html,保留 css、js 文件夹。本机数据仅属于当前浏览器;存储受限时页面会明确提示临时模式。

共享体验需要 Node.js 22 或更新版本,在项目目录运行 npm start,打开 http://127.0.0.1:8787。一个账号发布,另一个浏览器或无痕窗口中的账号查找,发布者更新后浏览者刷新即可看到结果。重启后账号和记录保留,会话需要重新登录。

发布表单

界面保留了纸色背景与固定位置的线索字段。寻物、招领、完成通过颜色和文字同时区分,时间和地点不需要展开卡片才能看到。图片与标题之外,仍然要靠具体的特征描述来判断是不是同一件物品。

八、提交记录与问题处理

1. 主仓库的功能提交

主仓库提交历史

主仓库已分别提交业务与本机保存、共享服务、页面完整流程、跨 shell 测试入口、文档与部署配置;742fec7 修正了仓库链接。个人 fork 上的 4e9d0c2 新增两项文件存储测试并更新文档,这些修改已通过 PR #1 合并进主仓库。这里的主仓库截图记录的是先前提交。

个人 fork 的测试提交与差异

上图是 GitHub 上 4e9d0c2 提交页的实际截图。下面是 PR #1 开放时的历史截图;该 PR 后来已合并,结果可在 PR 页面核对。

个人 fork 向主仓库提交的 PR #1

合并后的 GitHub 页面显示 PR 已合并到主仓库,合并提交为 669c4b8,两项检查通过。合并截图保存在个人 fork 的 docs/images/。

PR #1 合并完成

2. 从页面效果检查数据是否真的保存

原型中的新增记录只在内存里,刷新便消失。程序实现需要保存记录和稳定的本机身份;共享场景还需要服务端存储和会话。测试因此不只检查创建结果,也重新建立存储对象,确认身份、记录和完成状态仍在。

3. 草稿与已发布记录是不同生命周期

发布成功后清空草稿,但 hashchange 原先又保存了旧表单,导致再次进入时恢复已经发布的内容。解决方式是在成功提交后跳过一次离开时保存,普通离开仍保留文字。该问题的复核方式是发布后再次进入发布页,确认空表单和旧草稿清理。

4. 测试数量和测试范围分别说明

43 项里有单元测试,也有 HTTP 接口与集成测试,不能统称全部为单元测试。覆盖报告只统计 Node 加载模块,不能据此说页面所有事件都已经验证。仍缺少真实磁盘写满、所有图片解码和大规模数据的验证,报告中保留这些范围说明。

九、个人总结

我对这个题目更在意的是一条线索从开始到结束的状态。寻物和招领的文字不同,但都需要从处理中走到完成;状态更新之后,默认列表、详情和个人记录还要一起变化。如果只在一个页面换颜色,其他页面仍然停留在旧状态,用户就很难判断物品是否已经找到。

把规则提取成函数之后,检查问题的方式也更清楚。可以直接用一组输入说明“为什么接受”,再用另一组说明“为什么拒绝”。对于搜索,需要分清多个关键词是共同匹配还是任一匹配;对于权限,需要分清缺少身份和其他人的身份;对于保存,需要同时关注成功写入和失败后保留原数据。

测试中最值得保留的是失败路径的断言。标题过长时不应保存,配额不足时列表不应变化,别人的修改请求不应成功。失败返回只是其中一部分,还要确认没有留下错误数据。这比只看正常流程更接近同学真实使用时可能遇到的情况。

结对协作也应该围绕可核对的内容展开。页面需要哪些字段、接口怎样返回错误、状态由谁更新,最好在复核前写清楚;发现问题时,附上输入、操作和结果,会比一句“这里有问题”更容易讨论。个人提交和 PR 也应体现实际修改,不能只在博文中写一句参与了测试。

如果继续完善,我会优先补充真实磁盘写入错误、极端时间和输入长度,以及更多浏览器操作检查,再考虑增加功能。对这个项目而言,稳定地保存和更新一条线索,比增加许多暂时用不到的页面更有价值。

十、对结对同学的评价

  • 值得学习:从主仓库提交记录可以看到,姚华俊账号按业务规则、共享服务、页面流程和文档分阶段签入,README 也给出直接打开 HTML 与启动共享服务的步骤。这种按功能留痕的方式便于复核。
  • 改进建议:以后合并 PR 前,可以在评论中留下运行命令、测试结果和重点核对过的差异,让代码复核也能和提交记录对应起来。这里评价的是可见协作流程,线下配合仍以双方实际经历为准。

附录:白盒用例清单

编号 控制分支 / 方法 输入与预期
W01 有效发布、trim、初始状态 有效寻物带首尾空格;规范化并保存 open
W02 招领标签分支 found+open;招领中
W03 七项必填条件 逐项置空;对应字段错误
W04 空白等价类 普通空格、换行、全角空格;拒绝
W05 长度边界 标题 40 通过、41 拒绝
W06 时间解析与比较 无效日期、未来日期;拒绝
W07 图片格式 / 长度 SVG 和超长编码拒绝;允许光栅格式通过格式检查
W08 多词 every usb 黑色;同时满足才返回
W09 空查询和无结果 空白匹配全部,无关词匹配空数组
W10 类别、区域、类型、状态 四项共同筛选,仅预期记录匹配
W11 排序不改变原数组 事件时间降序,原序列不变
W12 状态变更成功 本人完成两类记录,再重新开放
W13 归属不符 他人、空身份、null;拒绝
W14 缺失记录 / 无效目标状态 404 / INVALID_INPUT
W15 重复状态 updatedAt 不变
W16 文本转义 标签和引号显示为文本
W17 配额异常 抛 STORAGE_FULL,旧列表不变
W18 损坏 JSON 原始内容不覆盖,进入临时模式
W19 存储禁止 可临时使用,明确告知关闭后丢失
W20 重建存储对象 发布身份、记录和完成状态保留

上述用例对应现有测试中的具体检查;接口层另检查未登录发布、重复注册、他人更新、跨站请求、私有路径、会话退出和并发事务。

参考资料

posted @ 2026-10-10 22:51  404coderbrain  阅读(5)  评论(0)    收藏  举报