软件工程第二次结对作业
这个作业属于哪个课程 | 软件工程 |
---|---|
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 项目实战 |
学号 | 072208130 |
结对伙伴 | 052205144张诗悦 |
结对同学的博客链接 | 软件工程第二次结对作业 |
一、准备工作⛵
(1)链接们
-
小队创建的仓库的GitHub项目地址:👉 Starryship/072208130-052205144: Here is the repository where we work together. (github.com)
-
小队的飞书协作知识库:👉 README.md - 飞书云文档 (feishu.cn)
-
该app使用uniCloud进行前端网页托管,已经部署到云服务器上了,想要使用该app可以点击以下链接直接使用:
①WEB:👉 https://static-mp-6cbf5855-4ae2-4eb0-9d2e-286e1c78e64a.next.bspapp.com/UniLink/index.html#/
点击以下链接即可安装:
②APP: 👉 https://github.com/Starryship/UniLink/releases/download/v1.0/UniLink.apk
移动端点击后可以直接安装,pc端点击则需将下载后的apk文件发送给移动端,使用移动端安装。(目前只提供安卓的安装包,ios个人开发者要花99$/年,孩纸负担不起捏~~)
(2)具体分工(小曹:曹星才,小张:张诗悦)
①前备工作:
小曹:创建与创作飞书协作知识库,创建github仓库,学习html、css、JavaScript、vue和uniapp的使用(学习资料均在知识库里,由小曹整理)
小张:创作飞书协作知识库,fork,学习html、css、JavaScript、vue和uniapp的使用(国庆无假期)
②页面分工:
小曹:demo3 demo4 demo5 demo6
小张:index demo1 demo2 demo7 demo8 demo9 demo10 demo11
共56条 Commits,具体可以看记录
③单元测试:小张
④软件打包与部署:小曹
⑤README文档、demo录制:小曹
⑥随笔:小张、小曹
二、ProjectPartner 🐟
我们选择在WEB、APP上部署我们的项目,项目功能:
①用户能够很好的体验注册登录的真实场景,用户对象包括导师以及学生,针对校内人员,干净实用
②用户能够发起自己的项目,同时能够对自己发起的项目可以进行管理,可招募校内各大专业的项目伙伴
③用户能够搜索并加入自己感兴趣的项目,受项目发起者管理
④用户能够添加合作伙伴为合作拍档,能够在“ProjectPartner”上与自己的项目成员进行实时交流,沟通项目的实施方案
⑤用户可通过点击伙伴头像能够查看伙伴资料
⑥用户可通过点击个人头像查看个人资料,个人资料可保存编辑
⑦用户能够通过隐私设置选择是否在资料卡上展示个人发起或参与的项目
三、解题思路描述与设计实现框架 🦐
(1)代码实现思路
1、代码流程
①用户注册和个人资料:
- 用户(学生)可以注册,并填写详细的个人资料,包括所属专业、技能、兴趣、可参与时间等。
- 允许用户发布或参与项目,同时可以设定项目的需求,比如需要的专业、技能、项目周期等。
②项目发起与参与:
- 学生可以创建项目并设置该项目的需求,比如需要哪些专业的支持(如编程、设计、市场营销等),以及对合作伙伴的要求。
- 学生可以搜索并申请加入其他人发起的项目,提供自己的专业技能。
③智能匹配:
- 系统可以根据用户的专业、技能和兴趣,推荐合适的项目或合作伙伴。
④项目管理与沟通:
- 平台可以提供项目管理工具,包括任务分配、时间进度管理、项目讨论区等。
- 跨专业的学生可以在平台上进行项目沟通,解决课程安排、项目目标差异等问题。
2、数据库设计:
①用户表:
- 存储用户的基本信息、专业、技能、兴趣等。
user_id
(主键)name
major
(所属专业)skills
(用户技能,如编程、设计、市场营销等)interests
(用户兴趣领域)availability
(可参与的时间段)
②项目表:
- 存储项目的详细信息,包括项目名称、发起人、所需的专业技能等。
project_id
(主键)project_name
(项目名称)project_description
(项目描述)initiator_id
(外键,指向项目发起人)required_skills
(所需的技能)duration
(项目周期)
③参与者表:
- 存储项目的参与者信息,方便进行匹配和管理。
participant_id
(主键)user_id
(外键,参与者)project_id
(外键,参与的项目)role
(参与者的角色,如编程、设计等)
3、前端设计
①项目浏览页面:
- 学生可以浏览当前发起的项目,并根据自己的专业、技能来筛选匹配的项目。
- 提供“申请加入”按钮,允许学生提交申请以参与项目。
②项目发起页面:
- 学生可以通过表单发布自己的项目,填写项目名称、描述、所需的专业和技能、项目周期等。
- 提供邀请功能,允许发起人直接邀请其他同学参与。
③个人资料页面:
- 展示用户的个人信息、专业、技能、参与的项目等。
- 允许用户编辑个人资料和技能,更新可参与时间等信息。
4、 功能实现
①项目创建与参与:
- 提供REST API接口,允许用户发起项目、修改项目、查看项目详情等。
- 用户可以通过申请加入其他项目,也可以自己创建项目,填充所需信息。
②智能推荐功能:
- 基于学生的专业、兴趣和技能,通过推荐算法匹配合适的项目或者合作伙伴。可以使用简单的关键字匹配或者基于用户历史的推荐系统。
③沟通与协作功能:
- 提供聊天或留言板功能,帮助跨专业学生在平台上交流合作。
- 集成项目管理功能,如任务分配、进度跟踪、文件共享等。
(2)关键实现的流程图或数据流图
(3)设计中有价值代码
①用户登录
<script>
export default {
data() {
return {
title: 'UniLink', //填写logo或者app名称,也可以用:欢迎回来,看您需求
second: 60, //默认60秒
showText: true, //判断短信是否发送
phone: '', //手机号码
yzm: '' //验证码
};
},
onLoad() {},
methods: {
//当前登录按钮操作
login() {
var that = this;
if (!that.phone) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
}
if (!/^[1][3,4,5,7,8,9][0-9]{9}$/.test(that.phone)) {
uni.showToast({ title: '请输入正确手机号', icon: 'none' });
return;
}
if (!that.yzm) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
//....此处省略,这里需要调用后台验证一下验证码是否正确,根据您的需求来
uni.showToast({ title: '登录成功!', icon: 'none' });
uni.switchTab({
url: "/pages/demo3/demo3"
})
},
//获取短信验证码
getCode() {
var that = this;
var interval = setInterval(() => {
that.showText = false;
var times = that.second - 1;
//that.second = times<10?'0'+times:times ;//小于10秒补 0
that.second = times;
console.log(times);
}, 1000);
setTimeout(() => {
clearInterval(interval);
that.second = 60;
that.showText = true;
}, 60000);
//这里请求后台获取短信验证码
uni.request({
//......//此处省略
success: function(res) {
that.showText = false;
}
});
},
//注册按钮点击
reg() {
uni.showToast({ title: '注册跳转', icon: 'none' });
uni.navigateTo({
url: "/pages/demo2/demo2"
})
},
//等三方微信登录
wxLogin() {
uni.showToast({ title: '微信登录', icon: 'none' });
},
//第三方支付宝登录
zfbLogin() {
uni.showToast({ title: 'QQ登录', icon: 'none' });
}
}
};
</script>
②项目发起与参与
<script>
import {
ref,
computed,
onMounted,
} from 'vue'; // 引入组合式 API 的 ref 和 computed 函数
// import { useRoute } from 'vue-router';
import { getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app'
export default {
setup() {
// 定义搜索框内容的响应式数据
const searchQuery = ref('');
// 定义项目列表数据
const projectList = ref([
{
id: 1,
name: '福uu',
domain: '软件工程',
requiredTalent: '软件工程师',
mentor: '翁谦',
time:"2023.10.12-2025.10.12",
inroduction: "福uu是FZU校内app,用于提供课程信息等",
target: "分析需求,设计app",
member: ["Bob","晨纸红","朝兴财","章师月"],
taskList: ["原型设计","编程实现"],
},
{
id: 2,
name: '联邦学习',
domain: '分布式',
requiredTalent: '大数据,机器学习',
mentor: '郭坤',
time:"2024.10.22-2025.05.12",
inroduction: "一个机器学习框架,能有效帮助多个机构在满足用户隐私保护、数据安全和政府法规的要求下,进行数据使用和机器学习建模。",
target: "优化算法",
member: ["Bob"],
taskList: ["横向联邦学习","纵向联邦学习","联邦迁移学习"],
},
]);
// 计算属性:根据搜索框内容动态过滤项目列表
const filteredProjects = computed(() => {
if (searchQuery.value.trim() === '') {
return projectList.value; // 如果搜索框为空,显示全部项目
}
// 过滤逻辑:根据项目名称或领域匹配搜索关键词
return projectList.value.filter(project =>
project.name.includes(searchQuery.value) ||
project.domain.includes(searchQuery.value)
);
});
// 定义搜索逻辑
const onSearch = () => {
console.log('搜索内容:', searchQuery.value); // 控制台输出当前搜索内容
uni.showToast({
title: `正在搜索:${searchQuery.value}`,
icon: 'none'
});
};
const pr_know_more = (id) => {
// 跳转到指定的项目详情页面,并传递项目 ID 作为查询参数
uni.navigateTo({
url: `/pages/demo4/demoManage/demoManage?id=${id}`,
success: () => {
console.log(`成功跳转到项目 ID 为 ${id} 的详情页面`);
},
fail: (err) => {
console.error('跳转失败:', err);
}
});
};
const handleClick2 = () => {
uni.navigateTo({
url: `/pages/demo5/demo5`,
success: () => {
console.log(`成功跳转到项目 ID 为 ${id} 的详情页面`);
},
fail: (err) => {
console.error('跳转失败:', err);
}
});
};
const fetchData = () => {
uni.getStorage({
key: 'projectData',
success: function(res) {
if (res.data) {
projectList.value.push(JSON.parse(res.data)); // 将数据解析并赋值给 projectList
}
},
fail: function(err) {
console.error('获取数据失败:', err);
}
});
};
fetchData(); // 调用函数获取数据
return {
searchQuery,
onSearch,
projectList,
filteredProjects,
pr_know_more,
handleClick2,
};
},
};
</script>
③页面传递参数时,在页面的生命周期内用钩子onMounted接收参数,渲染页面
<script>
import {
ref,onMounted,getCurrentInstance,computed} from 'vue'; // 引入 onMounted 和 getCurrentInstance
export default {
setup() {
// 响应式数据,用于存储项目详细信息
const project = ref(null);
// 模拟的项目数据(可以从接口或数据库中获取)
const projects = [{
id: 1,
name: '福uu',
domain: '软件工程',
requiredTalent: '软件工程师',
mentor: '翁谦',
time: "2023.10.12-2025.10.12",
inroduction: "福uu是FZU校内app,用于提供课程信息等",
target: "分析需求,设计app",
member: ["Bob", "晨纸红", "朝兴财", "章师月"],
taskList: ["原型设计", "编程实现"],
},
{
id: 2,
name: '联邦学习',
domain: '分布式',
requiredTalent: '大数据,机器学习',
mentor: '郭坤',
time: "2024.10.22-2025.05.12",
inroduction: "一个机器学习框架,能有效帮助多个机构在满足用户隐私保护、数据安全和政府法规的要求下,进行数据使用和机器学习建模。",
target: "优化算法",
member: ["Bob"],
taskList: ["横向联邦学习", "纵向联邦学习", "联邦迁移学习"],
},
];
onMounted(() => {
// 使用 getCurrentPages 获取当前页面栈
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1]; // 获取当前页面实例
// 从当前页面实例中获取路由参数 options
const options = currentPage.options;
const projectId = options.id; // 获取 id 参数
// 查找对应 ID 的项目数据
project.value = projects.find(p => p.id === Number(projectId)) || null;
if (!project.value) {
console.error(`未找到ID为 ${projectId} 的项目`);
}
});
// 将 taskList 转换为 checkboxItems
const checkboxItems = computed(() => {
// 确保在访问 taskList 之前,project.value 不是 null
if (project.value && project.value.taskList) {
return project.value.taskList.map(task => {
return {
name: task,
value: task,
checked: false
};
});
}
return []; // 如果 project.value 为空,返回空数组
});
const checkboxChange = (event) => {
console.log('选中的任务:', event.detail.value);
};
const handleClick = () => {
// alert('按钮被点击了');
// 显示提示框
uni.showToast({
title: '修改已保存', // 提示内容
icon: 'success', // 图标类型
duration: 2000 // 持续时间,单位为毫秒
});
};
return {
project,
checkboxItems,
checkboxChange,
handleClick,
};
},
};
</script>
四、功能 🐬
页面展示
功能展示
①注册
输入基本信息,注册账号
②申请项目
根据推荐的项目,可自主选择心仪的项目进行申请
③任务安排
对于已经参加的项目,我们可以选择管理,进行任务分配
④创建项目
可以进行新项目的创建
⑤聊天功能
可以此和合作伙伴一起聊天
⑥添加好友
添加新的合作伙伴
五、附加特点设计与展示 🐳
(一)个人资料与他人资料展示
①特点设计:查看老师同学以及可编辑个人资料
②实现思路:
查看老师、同学列表页面:
- 使用一个表格或者卡片式的布局来展示老师和同学的信息。
- 每个条目可以包含头像、姓名、角色(老师或同学)、基本信息等。
个人资料查看和编辑页面:
- 展示用户的详细信息,包括头像、姓名、邮箱、联系方式等。
- 如果用户是本人,页面上会有“编辑”按钮,点击后可以进入编辑模式,允许用户修改自己的个人信息,点击提交即可保存用户提交的信息。
查看老师和同学信息:
- 在聊天界面点击聊天对象的头像即可便捷查看
编辑个人资料功能:
- 为当前登录用户提供编辑按钮,点击后进入编辑模式。
- 允许用户更新个人资料,如修改邮箱、联系方式、简介等字段。
权限管理
- 确保用户只能编辑自己的资料,不能修改其他人的信息。
前端实现
-
使用框架Vue 来实现动态的界面和表单。
-
表单组件可以包括头像上传、文本输入等。
可选功能
-
头像上传:提供头像上传功能,支持用户更新个人的图片。
-
隐私设置:允许用户设置哪些信息对外公开,哪些信息只对自己可见。
③重要代码片段
有重要的/有价值的代码片段,这个代码是动态创建新项目的代码,是我第一次接触动态添加的代码,给了我很大的启发,后面的部分页面的部分功能都是基于这个部分做的
<script>
import { ref ,reactive} from "vue";
// import { useRouter } from 'vue-router';
export default {
setup() {
// 初始化项目列表
const projectList = ref([]);
// 新项目数据模板
const newProject = reactive({
name: "",
domain: "",
requiredTalent: "",
mentor: "",
time: "",
inroduction: "",
target: "",
memberInput: "",
taskListInput: "",
});
// 添加新项目到项目列表
const addProject = () => {
// console.log('addProject')
const id = projectList.value.length + 1; // 自动生成新项目的 ID
// console.log(newProject.value)
const members = newProject.memberInput.split(",").map((item) => item.trim());
const tasks = newProject.taskListInput.split(",").map((item) => item.trim());
// 构建新项目对象
const project = {
id,
name: newProject.name,
domain: newProject.domain,
requiredTalent: newProject.requiredTalent,
mentor: newProject.mentor,
time: newProject.time,
inroduction: newProject.inroduction,
target: newProject.target,
member: members,
taskList: tasks,
};
// 添加到项目列表
projectList.value.push(project);
// 清空表单
// 清空表单
for (const key in newProject) {
newProject[key] = ""; // 重置每个字段的值
}
console.log("新项目添加成功:", project);
console.log("当前项目列表:", projectList.value);
const projectData = JSON.stringify(project);
// 通过路由传递数据
console.log(projectData)
uni.setStorage({
key: 'projectData',
data: projectData,
success: function() {
uni.switchTab({
url: '/pages/demo4/demo4'
});
}
});
};
const submitForm = () => {
// 将 projectList 转换为 JSON 字符串
const projectData = JSON.stringify(newProject.value);
// 通过路由传递数据
console.log(projectData)
uni.switchTab({
url: `/pages/demo4/demo4?projects=${encodeURIComponent(projectData)}`
});
};
return {
projectList,
newProject,
addProject,
submitForm,
};
},
};
</script>
④实现成果展示
(二)个人项目发起与展示
①特点设计:填写信息发起项目,参与项目展示
②实现思路:
项目发起页面:
- 使用form表单进行信息传递
- 填写项目详细信息进行项目创建并发起
项目展示页面
- 使用卡片展示参与项目
- 使用flex布局
③重要代码片段
因为不同页面需要传递参数,即展示页面是用数组存储,但是表单提交也就是项目提交页面是在另一页,表单的提交就没有办法直接改变展示页面的数组,这个时候就得传递参数,利用uni.setStorage
对表单内容进行存储,在页面跳转时进行传递,然后在项目展示页面接收即可
<script>
import { ref ,reactive} from "vue";
// import { useRouter } from 'vue-router';
export default {
setup() {
// 初始化项目列表
const projectList = ref([]);
// 新项目数据模板
const newProject = reactive({
name: "",
domain: "",
requiredTalent: "",
mentor: "",
time: "",
inroduction: "",
target: "",
memberInput: "",
taskListInput: "",
});
// 添加新项目到项目列表
const addProject = () => {
// console.log('addProject')
const id = projectList.value.length + 1; // 自动生成新项目的 ID
// console.log(newProject.value)
const members = newProject.memberInput.split(",").map((item) => item.trim());
const tasks = newProject.taskListInput.split(",").map((item) => item.trim());
// 构建新项目对象
const project = {
id,
name: newProject.name,
domain: newProject.domain,
requiredTalent: newProject.requiredTalent,
mentor: newProject.mentor,
time: newProject.time,
inroduction: newProject.inroduction,
target: newProject.target,
member: members,
taskList: tasks,
};
// 添加到项目列表
projectList.value.push(project);
// 清空表单
for (const key in newProject) {
newProject[key] = ""; // 重置每个字段的值
}
console.log("新项目添加成功:", project);
console.log("当前项目列表:", projectList.value);
const projectData = JSON.stringify(project);
// 通过路由传递数据
console.log(projectData)
uni.setStorage({
key: 'projectData',
data: projectData,
success: function() {
uni.switchTab({
url: '/pages/demo4/demo4'
});
}
});
};
const submitForm = () => {
// 将 projectList 转换为 JSON 字符串
const projectData = JSON.stringify(newProject.value);
// 通过路由传递数据
console.log(projectData)
uni.switchTab({
url: `/pages/demo4/demo4?projects=${encodeURIComponent(projectData)}`
});
};
return {
projectList,
newProject,
addProject,
submitForm,
};
},
};
</script>
④实现成果展示
六、程序要求 🎣
1. GitHub仓库
链接:Starryship/072208130-052205144: Here is the repository where we work together. (github.com)
2. PSP
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 50 |
Estimate | 估计这个任务需要多少时间 | 5445 | 6319 |
Development | 开发 | 2800 | 5420 |
Analysis | 需求分析 (包括学习新技术) | 50 | 33 |
Design Spec | 生成设计文档 | 30 | 25 |
Design Review | 设计复审 | 10 | 18 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 3 |
Design | 具体设计 | 580 | 652 |
Coding | 具体编码 | 1690 | 4844 |
Code Review | 代码复审 | 30 | 5 |
Test | 测试(自我测试,修改代码,提交修改) | 60 | 64 |
Reporting | 报告 | 60 | 23 |
Test Repor | 测试报告 | 60 | 15 |
Size Measurement | 计算工作量 | 5 | 3 |
Postmortem&Process Improvement Plan | 事后总结, 并提出过程改进计划 | 10 | 8 |
合计 | 5445 | 6319 |
3. 目录结构
你们需要合理组织你自己的目录结构(HTML, CSS, JavaScript, 图片素材等,以及第三方框架),在博客中提供一份目录说明和使用说明,以供其他测试人员参照。
①十一个主页面在pages目录中
②HTML, CSS, JavaScript集成在各个页面上以
<template></template>
<script setup ></script>
<style lang="scss" scoped></style>
的结构进行组织
③所有的图片素材组织在static目录当中
④第三方框架:vue2、vue3
⑤具体使用说明见文档
👉Starryship/072208130-052205144: Here is the repository where we work together. (github.com)
4. README文档
使用markdown在GitHub项目中的README文档添加目录说明和使用说明:
👉Starryship/072208130-052205144: Here is the repository where we work together. (github.com)
5. 呈现形式
呈现形式不做要求,你可以使用WEB、APP或小程序。
①使用PC端web网页,使用谷歌浏览器,防止不同用户访问网页效果不一致:
👉 http://static-mp-6cbf5855-4ae2-4eb0-9d2e-286e1c78e64a.next.bspapp.com/#/
②使用APP,apk文件
👉 https://github.com/Starryship/UniLink/releases/download/v1.0/UniLink.apk
七、 单元测试 🐠
(1)测试工具: Python 的 unittest
(2)单元测试简易教程:
vue.js:在 Jest 中编写第一个 Vue.js 组件单元测试 |亚历克斯·乔弗 (alexjover.com)
良心推荐最下面这个,实打实的解决了问题:
vue3:vue3 jest单元测试环境搭建 - 知乎 (zhihu.com)
①测试用例结构
Setup:准备必要的测试数据或环境
Execution:执行要测试的代码
Assertion:检查执行结果是否符合预期
Teardown:清理测试环境
②测试工具的高级功能
Mocking:模拟对象或函数行为,隔离依赖
Parameterized Tests:使用不同参数进行重复测试
Coverage:使用工具检查代码覆盖率(如 pytest-cov、Jest 内置功能)
③持续集成与测试
将单元测试集成到持续集成(CI)工具中,如 GitHub Actions 或 Jenkins,保证代码每次提交后自动运行测试。
④ 实践与深入学习
完成小型项目的单元测试
尝试写边界条件测试,异常测试
探索工具的更多功能,如测试性能、异步测试等
(3)项目部分单元测试代码展示
①注册测试:
import { mount } from '@vue/test-utils';
import RegistrationPage from '@demo2.vue'; // 请替换为实际路径
import { jest } from '@jest/globals';
describe('RegistrationPage.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(RegistrationPage);
});
it('renders correctly', () => {
expect(wrapper.find('.logo').exists()).toBe(true);
expect(wrapper.find('input[type="tel"]').attributes('placeholder')).toBe('请输入手机号');
expect(wrapper.find('input[type="text"]').attributes('placeholder')).toBe('请输入图片验证码');
expect(wrapper.find('input[type="number"]').attributes('placeholder')).toBe('请输入验证码');
expect(wrapper.find('input[type="password"]').attributes('placeholder')).toBe('请输入密码');
expect(wrapper.find('button[type="primary"]').text()).toBe('注册');
});
it('button is disabled when captcha input is empty', () => {
const button = wrapper.find('button');
expect(button.attributes('disabled')).toBe('disabled');
// Simulate entering a captcha
wrapper.setData({ captchaImg: '1234' });
expect(button.attributes('disabled')).toBeUndefined();
});
it('sendCode method starts countdown', async () => {
jest.useFakeTimers();
wrapper.vm.sendCode();
expect(wrapper.vm.codeBtn.waitingCode).toBe(true);
expect(wrapper.vm.codeBtn.text).toBe('60s');
// Fast-forward the timer by 1 second
jest.advanceTimersByTime(1000);
expect(wrapper.vm.codeBtn.count).toBe(59);
expect(wrapper.vm.codeBtn.text).toBe('59s');
// Fast-forward to the end of the countdown
jest.advanceTimersByTime(60000);
expect(wrapper.vm.codeBtn.text).toBe('重新发送');
expect(wrapper.vm.codeBtn.waitingCode).toBe(false);
});
it('navigates to login page on gotoLogin', () => {
const navigateToMock = jest.fn();
uni.navigateTo = navigateToMock; // Mock uni.navigateTo
wrapper.vm.gotoLogin();
expect(navigateToMock).toHaveBeenCalledWith({
url: "/pages/demo1/demo1"
});
});
});
②登录测试
// 引入必要的依赖
import { shallowMount } from '@vue/test-utils';
import LoginPage from '@demo1.vue'; // 假设你的登录页面组件名为LoginPage.vue
import { jest } from '@jest/globals';
// Mock Uni-app的API
jest.mock('uni', () => ({
showToast: jest.fn(),
request: jest.fn(() => ({
success: (callback) => callback({ data: { success: true } })
})),
switchTab: jest.fn(),
navigateTo: jest.fn()
}));
describe('LoginPage.vue', () => {
let wrapper;
beforeEach(() => {
// 挂载组件
wrapper = shallowMount(LoginPage);
});
it('initializes with correct data', () => {
expect(wrapper.vm.title).toBe('UniLink');
expect(wrapper.vm.second).toBe(60);
expect(wrapper.vm.showText).toBe(true);
expect(wrapper.vm.phone).toBe('');
expect(wrapper.vm.yzm).toBe('');
});
it('shows toast when phone is not entered', async () => {
// 触发登录方法但不输入手机号
await wrapper.vm.login();
expect(uni.showToast).toHaveBeenCalledWith({ title: '请输入手机号', icon: 'none' });
});
it('shows toast when invalid phone is entered', async () => {
// 输入无效的手机号
wrapper.vm.phone = '123';
await wrapper.vm.login();
expect(uni.showToast).toHaveBeenCalledWith({ title: '请输入正确手机号', icon: 'none' });
});
it('shows toast when code is not entered', async () => {
// 输入有效的手机号但不输入验证码
wrapper.vm.phone = '13800138000';
await wrapper.vm.login();
expect(uni.showToast).toHaveBeenCalledWith({ title: '请输入验证码', icon: 'none' });
});
it('starts countdown when getCode is called', async () => {
// 调用getCode方法
await wrapper.vm.getCode();
// 验证showText变为false,并且second开始倒计时
expect(wrapper.vm.showText).toBe(false);
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待一秒
expect(wrapper.vm.second).toBe(59);
});
it('navigates to demo3 page on successful login', async () => {
// 输入有效的手机号和验证码
wrapper.vm.phone = '13800138000';
wrapper.vm.yzm = '123456';
// 模拟成功登录
await wrapper.vm.login();
expect(uni.switchTab).toHaveBeenCalledWith({ url: "/pages/demo3/demo3" });
});
});
③创建项目测试
import { mount } from '@vue/test-utils';
import ProjectComponent from '@demo5.vue'; // 请替换为实际路径
describe('ProjectComponent.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(ProjectComponent);
});
it('initializes with an empty project list', () => {
expect(wrapper.vm.projectList).toEqual([]);
});
it('adds a new project to the project list', async () => {
// Set up the new project data
wrapper.vm.newProject.name = 'Project 1';
wrapper.vm.newProject.domain = 'example.com';
wrapper.vm.newProject.requiredTalent = 'Developer';
wrapper.vm.newProject.mentor = 'Mentor Name';
wrapper.vm.newProject.time = '2024-01-01';
wrapper.vm.newProject.inroduction = 'Project introduction';
wrapper.vm.newProject.target = 'Target description';
wrapper.vm.newProject.memberInput = 'Member 1, Member 2';
wrapper.vm.newProject.taskListInput = 'Task 1, Task 2';
// Call the addProject method
await wrapper.vm.addProject();
// Check if the project has been added
expect(wrapper.vm.projectList).toHaveLength(1);
expect(wrapper.vm.projectList[0]).toMatchObject({
id: 1,
name: 'Project 1',
domain: 'example.com',
requiredTalent: 'Developer',
mentor: 'Mentor Name',
time: '2024-01-01',
inroduction: 'Project introduction',
target: 'Target description',
member: ['Member 1', 'Member 2'],
taskList: ['Task 1', 'Task 2'],
});
// Verify that the newProject fields are reset
expect(wrapper.vm.newProject.name).toBe('');
expect(wrapper.vm.newProject.memberInput).toBe('');
});
it('submits the form correctly', async () => {
// Mock the uni.switchTab function
global.uni = {
switchTab: jest.fn(),
};
// Set up the new project data
wrapper.vm.newProject.name = 'Project 1';
wrapper.vm.newProject.domain = 'example.com';
// Call the submitForm method
await wrapper.vm.submitForm();
// Check if the switchTab function was called
expect(global.uni.switchTab).toHaveBeenCalled();
expect(global.uni.switchTab).toHaveBeenCalledWith({
url: expect.stringContaining('/pages/demo4/demo4?projects='),
});
});
});
④消息通知测试
import { shallowMount } from '@vue/test-utils';
import ChatComponent from '@demo7'; // 替换为实际路径
describe('ChatComponent.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(ChatComponent, {
data() {
return {
contacts1: [
{
id: 0,
name: '共谋大事组',
avatar: '/static/toux1.jpg',
lastMessage: 'Hey, how are you?',
messages: [],
},
{
id: 1,
name: '张栋',
avatar: '/static/toux13.jpg',
lastMessage: 'Hey, how are you?',
messages: [],
},
// 添加更多联系人以满足测试需要
],
currentContact: null,
newMessage: '',
};
}
});
});
it('should select a contact correctly', async () => {
const contactToSelect = wrapper.vm.contacts1[0];
await wrapper.vm.selectContact(contactToSelect);
expect(wrapper.vm.currentContact).toBe(contactToSelect);
});
it('should send a message correctly', async () => {
wrapper.vm.currentContact = wrapper.vm.contacts1[0]; // 选择第一个联系人
wrapper.vm.newMessage = 'Hello World';
await wrapper.vm.sendMessage();
expect(wrapper.vm.currentContact.messages).toEqual(expect.arrayContaining([{
text: 'Hello World',
sender: 'me',
}]));
expect(wrapper.vm.newMessage).toBe('');
});
it('should not send an empty message', async () => {
wrapper.vm.currentContact = wrapper.vm.contacts1[0]; // 选择第一个联系人
wrapper.vm.newMessage = ''; // 设置为空
await wrapper.vm.sendMessage();
expect(wrapper.vm.currentContact.messages).toHaveLength(0); // 确保没有消息发送
});
it('should navigate to the correct contact', async () => {
const contactToNavigate = wrapper.vm.contacts1[1];
await wrapper.vm.nav(contactToNavigate);
// 测试是否正确调用 uni.navigateTo
expect(wrapper.vm.currentContact).toBe(contactToNavigate);
});
afterEach(() => {
wrapper.destroy();
});
});
(4)说明构造测试数据的思路,你是如何考虑各种情况的?你如何考虑将来测试人员的***难?
- 注册功能测试
- 输入:有效邮箱、密码
- 期望结果:成功注册并进入主界面
- 登录功能测试
- 输入:有效邮箱、密码
- 期望结果:成功登录
- 无效登录测试
- 输入:无效邮箱、错误密码
- 期望结果:提示错误信息,无法登录
- 忘记密码功能测试
- 输入:已注册邮箱
- 期望结果:收到密码重置邮件
- 项目创建功能测试
- 输入:有效的项目名称、描述、需求
- 期望结果:成功创建项目并显示在列表中
- 项目列表加载测试
- 输入:无
- 期望结果:正确显示所有已创建的项目
- 项目筛选功能测试
- 输入:特定关键词
- 期望结果:正确筛选出相关项目
- 项目详情查看功能测试
- 输入:点击项目
- 期望结果:正确显示项目详情
- 申请加入项目功能测试
- 输入:有效身份
- 期望结果:成功申请加入项目
- 消息通知功能测试
- 输入:项目负责人发送消息
- 期望结果:接收通知
- 消息通知 - 消息数量超过上限
- 输入:大量消息发送
- 期望结果:正确显示并分页处理
- 用户资料查看
- 输入:点击个人资料页
- 期望结果:个人信息展示正常
- 用户资料修改 - 正常流程
- 输入:修改用户信息
- 期望结果:修改成功
八、遇到的代码模块异常或结对困难及解决方法 🐚
①问题一:
-
问题描述:选择拍档进行聊天界面的定位联系人路由问题,点击无反应,控制台输出报错信息
-
做过哪些尝试:
uni.navigateTo()方法
uni.switchTab()方法
console.log()调试
-
是否解决
已解决
问题出现在初始化变量的位置
-
有何收获
后面的许多路由问题都能够独立解决
②问题二:
-
问题描述:从相机或相册上传头像问题,控制台一直报错:
①TypeError: Cannot read properties of undefined (reading 'width')
at upload.vue:11:24
at Proxy.renderFnWithContext (vue.runtime.esm.js:2033:13)
at Proxy.(uni-h5.es.js:15293:41)
at renderComponentRoot (vue.runtime.esm.js:2092:17)
at ReactiveEffect.componentUpdateFn [as fn] (vue.runtime.esm.js:7321:46)
at ReactiveEffect.run (vue.runtime.esm.js:153:19)
at instance.update (vue.runtime.esm.js:7453:17)
at setupRenderEffect (vue.runtime.esm.js:7463:5)
at mountComponent (vue.runtime.esm.js:7230:7)
at processComponent (vue.runtime.esm.js:7184:9)②SyntaxError: The requested module '/static/weCropper.js?t=1728221570874' does not provide an export named 'default' (at upload.vue:22:8)
怎么调都没法子,有没有懂的教教孩儿
-
做过哪些尝试:查博客,按网上的方法无用,问ai,ai也无用,自己仔仔细细改,越改越乱,但路由能够成功,也能打开系统相册
-
是否解决
待解决
-
有何收获
提高了我自学以及解决问题的能力
九、队友互评 🐙
①小曹评小张:
- 值得学习的地方:做事认真不推脱,非常好学、有上进行,安排的任务都能认真完成,很认真的做出反馈提出意见,做事情很快,高效且有质量,是脑力与体力担当!总之是一个非常不错的队友
- 需要改进的地方:很完美
②小张评小曹:
- 值得学习的地方:非常认真且仔细的完成任务,不偷懒,不找借口逃避问题,很上进,很优秀,做事有条理有节奏,很会搜罗学习资料,经常在我遇到问题时远程控制我的桌面为我解决问题,真心遇到了一个很好的队友
- 需要改进的地方:我认为目前还没看到