
在软件测试领域单元测试Unit Testing简称UT是保证代码质量的关键环节。最近在开发一个名为UBBUP的测试管理工具时遇到了主界面设置中Act或阶段选择功能的设计挑战——如何让测试人员快速切换不同的测试阶段同时保持界面简洁易用。本文将完整分享UBBUP主界面设置模块的实现方案包含从需求分析到代码落地的全流程适合测试开发工程师和全栈开发者参考。1. UBBUP工具与测试阶段选择需求分析1.1 UBBUP工具定位与核心功能UBBUP是一个专为测试团队设计的测试管理平台主要功能包括测试用例管理、测试计划制定、测试执行跟踪和测试报告生成。工具采用B/S架构前端使用Vue.jsElement UI后端采用Spring Boot框架。在主界面设计中测试阶段Act选择功能是核心交互点用户需要根据测试进度在不同阶段间快速切换。1.2 测试阶段Act的业务含义在UBBUP中Act代表测试活动的不同阶段例如Act 1单元测试阶段Act 2集成测试阶段Act 3系统测试阶段Act 4验收测试阶段每个Act对应不同的测试用例集、测试环境和权限控制。测试人员根据项目进度选择对应的Act系统会自动过滤相关测试资源确保测试活动的有序进行。1.3 主界面设置模块的技术挑战主界面设置模块需要解决以下技术难点动态加载可用的测试阶段列表记住用户最后一次选择的阶段实时切换阶段的界面状态更新权限控制不同角色可见的阶段不同响应式设计适配不同屏幕尺寸2. 环境准备与技术栈说明2.1 开发环境要求操作系统Windows 10/11 或 macOS 10.14前端开发Node.js 16.0Vue 3.0Element Plus 2.0后端开发JDK 11Spring Boot 2.7Maven 3.6数据库MySQL 8.0 或 PostgreSQL 14IDE推荐VS Code前端 IntelliJ IDEA后端2.2 项目结构说明ubbup-test-platform/ ├── frontend/ # 前端项目 │ ├── src/ │ │ ├── components/ # 组件目录 │ │ │ └── Layout/ # 布局组件 │ │ ├── views/ # 页面视图 │ │ ├── store/ # 状态管理 │ │ └── api/ # API接口 ├── backend/ # 后端项目 │ ├── src/main/java/ │ │ ├── controller/ # 控制层 │ │ ├── service/ # 业务层 │ │ ├── mapper/ # 数据层 │ │ └── entity/ # 实体类 │ └── src/main/resources/ # 配置文件2.3 关键依赖配置前端package.json关键依赖{ dependencies: { vue: ^3.2.0, vue-router: ^4.0.0, element-plus: ^2.0.0, axios: ^1.0.0, pinia: ^2.0.0 } }后端pom.xml关键依赖dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies3. 数据库设计与实体关系3.1 测试阶段Act表结构设计CREATE TABLE test_act ( id BIGINT PRIMARY KEY AUTO_INCREMENT, act_code VARCHAR(50) NOT NULL COMMENT 阶段编码, act_name VARCHAR(100) NOT NULL COMMENT 阶段名称, description TEXT COMMENT 阶段描述, display_order INT DEFAULT 0 COMMENT 显示顺序, is_active BOOLEAN DEFAULT true COMMENT 是否启用, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_act_code (act_code) ) COMMENT测试阶段表; CREATE TABLE user_act_preference ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, act_code VARCHAR(50) NOT NULL COMMENT 最后选择的阶段编码, last_selected_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (act_code) REFERENCES test_act(act_code) ) COMMENT用户阶段偏好设置;3.2 实体类设计后端Java实体类Entity Table(name test_act) public class TestAct { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name act_code, unique true, nullable false) private String actCode; Column(name act_name, nullable false) private String actName; private String description; private Integer displayOrder; private Boolean isActive; CreationTimestamp private LocalDateTime createdTime; UpdateTimestamp private LocalDateTime updatedTime; // getters and setters } Entity Table(name user_act_preference) public class UserActPreference { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; ManyToOne JoinColumn(name user_id) private User user; private String actCode; private LocalDateTime lastSelectedTime; // getters and setters }4. 后端API接口实现4.1 获取可用测试阶段列表RestController RequestMapping(/api/act) public class ActController { Autowired private ActService actService; GetMapping(/list) public ResponseEntityListTestAct getActiveActs( AuthenticationPrincipal User user) { ListTestAct acts actService.getActiveActs(user); return ResponseEntity.ok(acts); } } Service public class ActService { Autowired private TestActRepository actRepository; public ListTestAct getActiveActs(User user) { // 根据用户权限过滤可访问的阶段 return actRepository.findByIsActiveTrueOrderByDisplayOrderAsc(); } }4.2 保存用户阶段选择偏好PostMapping(/preference) public ResponseEntityVoid saveActPreference( RequestBody ActPreferenceRequest request, AuthenticationPrincipal User user) { actService.saveUserPreference(user, request.getActCode()); return ResponseEntity.ok().build(); } Data public class ActPreferenceRequest { NotBlank private String actCode; }4.3 获取用户最后选择的阶段GetMapping(/preference/current) public ResponseEntityString getCurrentActPreference( AuthenticationPrincipal User user) { String actCode actService.getUserCurrentAct(user); return ResponseEntity.ok(actCode); }5. 前端主界面设置组件实现5.1 阶段选择器组件封装template el-dropdown commandhandleActChange triggerclick classact-selector span classact-display {{ currentActName }} el-iconarrow-down //el-icon /span template #dropdown el-dropdown-menu el-dropdown-item v-foract in actList :keyact.actCode :commandact.actCode :class{ active: act.actCode currentAct } span classact-item el-iconcheck v-ifact.actCode currentAct //el-icon {{ act.actName }} /span /el-dropdown-item /el-dropdown-menu /template /el-dropdown /template script setup import { ref, onMounted, computed } from vue import { ElMessage } from element-plus import { useActStore } from /stores/act const actStore useActStore() const currentAct ref() const actList ref([]) const currentActName computed(() { const act actList.value.find(item item.actCode currentAct.value) return act ? act.actName : 选择测试阶段 }) const loadActList async () { try { await actStore.loadActs() actList.value actStore.actList currentAct.value actStore.currentAct } catch (error) { ElMessage.error(加载测试阶段失败) } } const handleActChange async (actCode) { try { await actStore.setCurrentAct(actCode) currentAct.value actCode ElMessage.success(已切换到${currentActName.value}) // 触发全局阶段变更事件 window.dispatchEvent(new CustomEvent(act-changed, { detail: { actCode } })) } catch (error) { ElMessage.error(切换测试阶段失败) } } onMounted(() { loadActList() }) /script style scoped .act-selector { cursor: pointer; padding: 8px 12px; border-radius: 4px; background: #f5f7fa; } .act-display { display: flex; align-items: center; gap: 4px; font-weight: 500; } .act-item { display: flex; align-items: center; gap: 8px; min-width: 120px; } .active { background-color: #ecf5ff; } /style5.2 状态管理Pinia// stores/act.js import { defineStore } from pinia import { ref } from vue import { getActList, setActPreference, getCurrentAct } from /api/act export const useActStore defineStore(act, () { const actList ref([]) const currentAct ref() const loadActs async () { try { const response await getActList() actList.value response.data // 获取用户最后选择的阶段 const currentResponse await getCurrentAct() currentAct.value currentResponse.data || (actList.value[0]?.actCode || ) } catch (error) { console.error(加载测试阶段失败:, error) throw error } } const setCurrentAct async (actCode) { try { await setActPreference({ actCode }) currentAct.value actCode } catch (error) { console.error(设置测试阶段失败:, error) throw error } } return { actList, currentAct, loadActs, setCurrentAct } })5.3 API接口封装// api/act.js import request from /utils/request export const getActList () { return request({ url: /api/act/list, method: get }) } export const setActPreference (data) { return request({ url: /api/act/preference, method: post, data }) } export const getCurrentAct () { return request({ url: /api/act/preference/current, method: get }) }6. 主界面布局集成6.1 顶部导航栏集成阶段选择器template div classmain-layout header classheader div classheader-left h1 classlogoUBBUP测试平台/h1 /div div classheader-center ActSelector / /div div classheader-right UserInfo / /div /header main classmain-content router-view / /main /div /template script setup import ActSelector from /components/Layout/ActSelector.vue import UserInfo from /components/Layout/UserInfo.vue /script style scoped .main-layout { height: 100vh; display: flex; flex-direction: column; } .header { display: flex; align-items: center; justify-content: space-between; padding: 0 24px; height: 60px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-bottom: 1px solid #e4e7ed; } .header-center { flex: 1; display: flex; justify-content: center; } .main-content { flex: 1; overflow: auto; background: #f5f7fa; } /style6.2 阶段变更的全局响应// main.js或App.vue中监听阶段变更 window.addEventListener(act-changed, (event) { const { actCode } event.detail // 更新页面标题 document.title UBBUP - ${getActName(actCode)} // 刷新相关数据 refreshTestData(actCode) // 更新路由参数如果需要 updateRouteParams(actCode) }) const refreshTestData (actCode) { // 根据阶段代码重新加载测试数据 console.log(阶段已切换到: ${actCode}) // 这里可以调用各个组件的刷新方法 }7. 权限控制与数据过滤7.1 基于阶段的权限控制Service public class ActPermissionService { public boolean hasActAccess(User user, String actCode) { // 检查用户是否有权限访问该测试阶段 SetString userRoles getUserRoles(user); TestAct act actRepository.findByActCode(actCode); if (act null || !act.getIsActive()) { return false; } // 根据角色和阶段配置判断权限 return actPermissionRepository.existsByActCodeAndRoleIn(actCode, userRoles); } public ListTestAct getAccessibleActs(User user) { SetString userRoles getUserRoles(user); return actPermissionRepository.findAccessibleActs(userRoles); } }7.2 前端路由守卫// router/guards.js import { useActStore } from /stores/act export const actGuard (to, from, next) { const actStore useActStore() // 确保阶段数据已加载 if (!actStore.currentAct) { actStore.loadActs().then(() { validateActAccess(to, next, actStore) }).catch(() { next(/error) }) } else { validateActAccess(to, next, actStore) } } const validateActAccess (to, next, actStore) { const requiredAct to.meta?.requiredAct if (requiredAct requiredAct ! actStore.currentAct) { // 重定向到当前阶段对应的页面 next({ name: to.name, params: { ...to.params, act: actStore.currentAct } }) } else { next() } }8. 性能优化与用户体验8.1 阶段数据缓存策略// 使用localStorage缓存阶段数据 const ACT_CACHE_KEY ubbup_act_cache export const useActStore defineStore(act, () { // ...其他代码 const loadActs async () { // 先尝试从缓存读取 const cached localStorage.getItem(ACT_CACHE_KEY) if (cached) { const cacheData JSON.parse(cached) if (Date.now() - cacheData.timestamp 5 * 60 * 1000) { // 5分钟缓存 actList.value cacheData.actList await loadCurrentAct() return } } // 缓存失效从服务器加载 try { const response await getActList() actList.value response.data // 更新缓存 localStorage.setItem(ACT_CACHE_KEY, JSON.stringify({ actList: response.data, timestamp: Date.now() })) await loadCurrentAct() } catch (error) { console.error(加载测试阶段失败:, error) throw error } } })8.2 加载状态与错误处理template el-dropdown :disabledloading commandhandleActChange span classact-display el-icon v-ifloading classis-loadingloading //el-icon {{ currentActName }} el-iconarrow-down //el-icon /span template #dropdown el-dropdown-menu el-dropdown-item v-foract in actList :keyact.actCode :commandact.actCode {{ act.actName }} /el-dropdown-item /el-dropdown-menu /template /el-dropdown /template9. 测试与验证9.1 单元测试用例// tests/unit/ActSelector.spec.js import { mount } from vue/test-utils import ActSelector from /components/Layout/ActSelector.vue import { createTestingPinia } from pinia/testing describe(ActSelector组件测试, () { it(应该正确显示当前选择的阶段, async () { const wrapper mount(ActSelector, { global: { plugins: [createTestingPinia({ initialState: { act: { actList: [ { actCode: act1, actName: 单元测试 }, { actCode: act2, actName: 集成测试 } ], currentAct: act1 } } })] } }) expect(wrapper.text()).toContain(单元测试) }) it(切换阶段应该触发相应事件, async () { const wrapper mount(ActSelector, { global: { plugins: [createTestingPinia()] } }) await wrapper.find(.act-selector).trigger(click) await wrapper.findAll(.el-dropdown-item)[1].trigger(click) // 验证阶段变更事件是否触发 expect(wrapper.emitted()).toHaveProperty(act-changed) }) })9.2 集成测试SpringBootTest class ActControllerIntegrationTest { Autowired private TestRestTemplate restTemplate; Test void shouldReturnActiveActs() { // 模拟用户登录 HttpHeaders headers createAuthHeaders(); ResponseEntityList response restTemplate.exchange( /api/act/list, HttpMethod.GET, new HttpEntity(headers), List.class ); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); } }10. 常见问题与解决方案10.1 阶段切换后页面数据不更新问题现象切换测试阶段后页面显示的数据仍然是上一个阶段的内容。解决方案在阶段变更事件中强制刷新组件使用Vue的forceUpdate方法在路由守卫中检查阶段一致性// 强制刷新当前页面数据 const refreshCurrentPage () { const currentRoute router.currentRoute.value router.replace({ path: /redirect currentRoute.fullPath }).then(() { router.replace(currentRoute) }) }10.2 权限控制失效问题现象用户可以看到但没有权限访问的阶段。解决方案后端接口增加权限验证前端路由守卫双重验证定期同步用户权限信息GetMapping(/list) public ResponseEntityListTestAct getActiveActs( AuthenticationPrincipal User user) { // 只返回用户有权限访问的阶段 ListTestAct acts actService.getAccessibleActs(user); return ResponseEntity.ok(acts); }10.3 移动端适配问题问题现象在小屏幕设备上阶段选择器显示异常。解决方案使用响应式设计添加移动端专属样式测试不同屏幕尺寸的显示效果/* 移动端适配 */ media (max-width: 768px) { .act-selector { padding: 6px 8px; font-size: 14px; } .act-item { min-width: 100px; } }11. 最佳实践与工程建议11.1 代码组织规范将阶段选择功能封装为独立组件便于复用使用Pinia进行状态管理确保数据一致性API接口统一封装便于维护和Mock测试11.2 性能优化建议阶段数据适当缓存减少服务器请求使用防抖技术避免频繁的阶段切换懒加载阶段相关的资源文件11.3 安全考虑后端验证用户对每个阶段的访问权限敏感操作记录日志定期审计权限配置11.4 可维护性建议使用TypeScript增强类型安全编写详细的组件文档建立完整的测试覆盖通过本文的完整实现方案UBBUP测试平台的主界面设置功能能够为用户提供流畅的测试阶段切换体验。关键点在于前后端的协同设计、权限控制的严谨性以及用户体验的优化。在实际项目中还可以根据具体需求扩展更多功能如阶段自定义、批量操作等。