27 KiB
27 KiB
设计文档 - 测试执行
元数据
- 需求编号:002-03
- 创建时间:2026-02-02
- 创建人:AI Assistant
- 状态:已完成(代码已生成)
设计概述
本设计文档描述 Apex 测试执行功能的完整技术方案。测试执行功能允许用户在 Salesforce 平台上运行 Apex 测试,支持多种运行方式(全部测试、指定类、指定包、指定方法),并提供详细的测试结果、代码覆盖率和 Flow 覆盖率分析。
设计目标
- 提供灵活的测试执行方式(全部、按类、按包、按方法)
- 详细记录测试结果(成功、失败、覆盖率)
- 支持代码覆盖率和 Flow 覆盖率分析
- 确保多线程安全和数据一致性
- 提供 RESTful API 接口供外部调用
与相关功能的关系
- 依赖 002-01(连接管理):使用 ApexConnectionFactory 获取 SoapConnection
- 复用 002-02(编译和执行):复用 ApexTestResult、ApexTestFailure、ApexCodeCoverage 表
- 新建表:ApexTestSuccess(测试成功详情)、ApexFlowCoverage(Flow 覆盖率)
架构设计
系统架构
┌─────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ (Vue 3 + Element Plus / API 调用) │
└───────────────────────┬─────────────────────────────────────┘
│ HTTP/HTTPS
┌───────────────────────▼─────────────────────────────────────┐
│ Controller 层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ApexTest │ │ApexTest │ │ApexTest │ │
│ │Controller │ │Query │ │Coverage │ │
│ │(执行测试) │ │Controller │ │Controller │ │
│ └──────────────┘ │(查询结果) │ │(查询覆盖率) │ │
│ └──────────────┘ └──────────────┘ │
└───────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────────┐
│ Service 层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ IApexTestService │ │
│ │ - runAllTests() │ │
│ │ - runTestsByClasses(String[] classes) │ │
│ │ - runTestsByPackages(String[] packages) │ │
│ │ - runTestsByMethods(TestsNode[] tests) │ │
│ │ - runTests(RunTestsRequest request) │ │
│ │ - queryTestResults(...) │ │
│ │ - queryTestDetails(...) │ │
│ │ - queryCodeCoverage(...) │ │
│ │ - queryFlowCoverage(...) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ApexTestServiceImpl │ │
│ │ - ReentrantLock 保证多线程安全 │ │
│ │ - @Transactional 保证数据一致性 │ │
│ │ - 保存测试结果到数据库 │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────────┐
│ Factory 层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ApexConnectionFactory │ │
│ │ - getConnection(String orgType) │ │
│ │ - 复用 002-01 的连接管理机制 │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────────┐
│ Salesforce API 层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SoapConnection (apex.jar) │ │
│ │ - runTests(RunTestsRequest request) │ │
│ │ - RunTestsResult / RunTestSuccess / RunTestFailure │ │
│ │ - CodeCoverageResult / FlowCoverageResult │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
模块架构
模块位置:datai-salesforce-apex 模块
包结构:
com.datai.apex
├── controller
│ ├── ApexTestController.java # 测试执行接口
│ ├── ApexTestQueryController.java # 测试结果查询接口
│ └── ApexTestCoverageController.java # 覆盖率查询接口
├── service
│ ├── IApexTestService.java # 测试服务接口
│ └── impl
│ └── ApexTestServiceImpl.java # 测试服务实现
├── model
│ ├── dto
│ │ ├── RunAllTestsDto.java # 运行所有测试请求
│ │ ├── RunTestsByClassesDto.java # 按类运行测试请求
│ │ ├── RunTestsByPackagesDto.java # 按包运行测试请求
│ │ ├── RunTestsByMethodsDto.java # 按方法运行测试请求
│ │ └── QueryTestResultsDto.java # 查询测试结果请求
│ └── vo
│ ├── RunTestsResultVo.java # 测试结果 VO
│ ├── RunTestSuccessVo.java # 测试成功 VO
│ ├── RunTestFailureVo.java # 测试失败 VO
│ ├── CodeCoverageResultVo.java # 代码覆盖率 VO
│ └── FlowCoverageResultVo.java # Flow 覆盖率 VO
└── mapper
├── DataiApexTestSuccessMapper.java # 测试成功详情 Mapper(新建)
└── DataiApexFlowCoverageMapper.java # Flow 覆盖率 Mapper(新建)
数据流
1. 客户端发送测试执行请求
↓
2. Controller 接收请求,参数校验
↓
3. Service 获取 SoapConnection(复用 002-01)
↓
4. Service 调用 Salesforce API 执行测试
↓
5. Service 解析测试结果(RunTestsResult)
↓
6. Service 保存测试结果到数据库(5 张表)
↓
7. Service 返回测试结果给 Controller
↓
8. Controller 返回统一响应格式给客户端
技术方案
技术选型
| 技术组件 | 选型 | 说明 |
|---|---|---|
| 连接管理 | ApexConnectionFactory | 复用 002-01 的连接管理机制 |
| 多线程安全 | ReentrantLock | 保证测试执行的多线程安全 |
| 事务管理 | @Transactional | 保证测试结果保存的数据一致性 |
| 权限控制 | Spring Security @PreAuthorize | 接口级别的权限控制 |
| 数据持久化 | MyBatis | 使用标准 MyBatis(非 MyBatis Plus) |
| API 文档 | Swagger | 自动生成 API 文档 |
核心算法
1. 测试结果保存算法
@Transactional(rollbackFor = Exception.class)
public void saveTestResults(RunTestsResult result) {
// 1. 保存测试结果主表
DataiApexTestResult testResult = convertToTestResult(result);
testResultMapper.insert(testResult);
// 2. 保存测试成功详情
if (result.getSuccesses() != null) {
for (RunTestSuccess success : result.getSuccesses()) {
DataiApexTestSuccess testSuccess = convertToTestSuccess(success, testResult.getId());
testSuccessMapper.insert(testSuccess);
}
}
// 3. 保存测试失败详情
if (result.getFailures() != null) {
for (RunTestFailure failure : result.getFailures()) {
DataiApexTestFailure testFailure = convertToTestFailure(failure, testResult.getId());
testFailureMapper.insert(testFailure);
}
}
// 4. 保存代码覆盖率
if (result.getCodeCoverage() != null) {
for (CodeCoverageResult coverage : result.getCodeCoverage()) {
DataiApexCodeCoverage codeCoverage = convertToCodeCoverage(coverage, testResult.getId());
codeCoverageMapper.insert(codeCoverage);
}
}
// 5. 保存 Flow 覆盖率
if (result.getFlowCoverage() != null) {
for (FlowCoverageResult flowCoverage : result.getFlowCoverage()) {
DataiApexFlowCoverage flowCov = convertToFlowCoverage(flowCoverage, testResult.getId());
flowCoverageMapper.insert(flowCov);
}
}
}
2. 覆盖率计算算法
// 代码覆盖率计算
double coveragePercent = (1.0 - (double) numLocationsNotCovered / numLocations) * 100;
// Flow 覆盖率计算
double flowCoveragePercent = (double) numElementsCovered / numElements * 100;
集成方案
与 Salesforce API 集成
// 1. 构建 RunTestsRequest
RunTestsRequest request = new RunTestsRequest();
request.setAllTests(true); // 或设置 classes/packages/tests
request.setSkipCodeCoverage(false);
request.setMaxFailedTests(0);
// 2. 获取连接并执行测试
SoapConnection connection = apexConnectionFactory.getConnection("source");
RunTestsResult result = connection.runTests(request);
// 3. 解析结果
int numTestsRun = result.getNumTestsRun();
int numFailures = result.getNumFailures();
RunTestSuccess[] successes = result.getSuccesses();
RunTestFailure[] failures = result.getFailures();
CodeCoverageResult[] codeCoverage = result.getCodeCoverage();
FlowCoverageResult[] flowCoverage = result.getFlowCoverage();
数据模型
数据库表设计
1. datai_apex_test_result(测试结果表)- 复用 002-02
| 字段名 | 类型 | 长度 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| id | BIGINT | - | 是 | 自增 | 主键 |
| test_run_id | VARCHAR | 50 | 否 | - | 测试运行 ID |
| num_tests_run | INT | - | 否 | 0 | 运行的测试数量 |
| num_failures | INT | - | 否 | 0 | 失败的测试数量 |
| total_time | DOUBLE | - | 否 | 0 | 总执行时间(毫秒) |
| apex_log_id | VARCHAR | 50 | 否 | - | Apex 日志 ID |
| test_time | DATETIME | - | 否 | - | 测试时间 |
| all_passed | BOOLEAN | - | 否 | false | 是否全部通过 |
| total_coverage | DOUBLE | - | 否 | 0 | 总体覆盖率 |
| dept_id | BIGINT | - | 否 | 0 | 部门 ID |
| create_by | VARCHAR | 64 | 否 | - | 创建者 |
| create_time | DATETIME | - | 否 | CURRENT_TIMESTAMP | 创建时间 |
| update_by | VARCHAR | 64 | 否 | - | 更新者 |
| update_time | DATETIME | - | 否 | CURRENT_TIMESTAMP | 更新时间 |
| del_flag | TINYINT | - | 否 | 0 | 删除标志 |
索引:
- PRIMARY KEY (
id) - INDEX
idx_test_run_id(test_run_id) - INDEX
idx_test_time(test_time) - INDEX
idx_create_by(create_by) - INDEX
idx_dept_id(dept_id)
2. datai_apex_test_success(测试成功详情表)- 新建
| 字段名 | 类型 | 长度 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| id | BIGINT | - | 是 | 自增 | 主键 |
| test_result_id | BIGINT | - | 是 | - | 测试结果 ID(外键) |
| test_id | VARCHAR | 50 | 否 | - | 测试 ID |
| class_name | VARCHAR | 255 | 否 | - | 类名 |
| method_name | VARCHAR | 255 | 否 | - | 方法名 |
| time | DOUBLE | - | 否 | 0 | 执行时间(毫秒) |
| message | TEXT | - | 否 | - | 消息 |
| stack_trace | TEXT | - | 否 | - | 堆栈跟踪 |
| create_time | DATETIME | - | 否 | CURRENT_TIMESTAMP | 创建时间 |
| del_flag | TINYINT | - | 否 | 0 | 删除标志 |
索引:
- PRIMARY KEY (
id) - INDEX
idx_test_result_id(test_result_id) - INDEX
idx_class_name(class_name) - INDEX
idx_method_name(method_name)
3. datai_apex_test_failure(测试失败详情表)- 复用 002-02
| 字段名 | 类型 | 长度 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| id | BIGINT | - | 是 | 自增 | 主键 |
| test_result_id | BIGINT | - | 是 | - | 测试结果 ID(外键) |
| test_id | VARCHAR | 50 | 否 | - | 测试 ID |
| class_name | VARCHAR | 255 | 否 | - | 类名 |
| method_name | VARCHAR | 255 | 否 | - | 方法名 |
| message | TEXT | - | 否 | - | 失败消息 |
| stack_trace | TEXT | - | 否 | - | 堆栈跟踪 |
| type | VARCHAR | 255 | 否 | - | 异常类型 |
| time | DOUBLE | - | 否 | 0 | 执行时间(毫秒) |
| create_time | DATETIME | - | 否 | CURRENT_TIMESTAMP | 创建时间 |
| del_flag | TINYINT | - | 否 | 0 | 删除标志 |
索引:
- PRIMARY KEY (
id) - INDEX
idx_test_result_id(test_result_id) - INDEX
idx_class_name(class_name) - INDEX
idx_method_name(method_name)
4. datai_apex_code_coverage(代码覆盖率表)- 复用 002-02
| 字段名 | 类型 | 长度 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| id | BIGINT | - | 是 | 自增 | 主键 |
| test_result_id | BIGINT | - | 是 | - | 测试结果 ID(外键) |
| name | VARCHAR | 255 | 否 | - | 类/触发器名称 |
| type | VARCHAR | 20 | 否 | - | 类型(Class/Trigger) |
| namespace | VARCHAR | 255 | 否 | - | 命名空间 |
| num_locations | INT | - | 否 | 0 | 总位置数 |
| num_locations_not_covered | INT | - | 否 | 0 | 未覆盖的位置数 |
| coverage_percent | DOUBLE | - | 否 | 0 | 覆盖率百分比 |
| create_time | DATETIME | - | 否 | CURRENT_TIMESTAMP | 创建时间 |
| del_flag | TINYINT | - | 否 | 0 | 删除标志 |
索引:
- PRIMARY KEY (
id) - INDEX
idx_test_result_id(test_result_id) - INDEX
idx_name(name) - INDEX
idx_type(type)
5. datai_apex_flow_coverage(Flow 覆盖率表)- 新建
| 字段名 | 类型 | 长度 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| id | BIGINT | - | 是 | 自增 | 主键 |
| test_result_id | BIGINT | - | 是 | - | 测试结果 ID(外键) |
| name | VARCHAR | 255 | 否 | - | Flow 名称 |
| type | VARCHAR | 50 | 否 | - | Flow 类型 |
| namespace | VARCHAR | 255 | 否 | - | 命名空间 |
| num_elements | INT | - | 否 | 0 | 总元素数量 |
| num_elements_covered | INT | - | 否 | 0 | 覆盖的元素数量 |
| coverage_percent | DOUBLE | - | 否 | 0 | 覆盖率百分比 |
| create_time | DATETIME | - | 否 | CURRENT_TIMESTAMP | 创建时间 |
| del_flag | TINYINT | - | 否 | 0 | 删除标志 |
索引:
- PRIMARY KEY (
id) - INDEX
idx_test_result_id(test_result_id) - INDEX
idx_name(name) - INDEX
idx_type(type)
实体类设计
1. DataiApexTestSuccess(测试成功详情实体)- 新建
@Data
public class DataiApexTestSuccess {
private Long id;
private Long testResultId;
private String testId;
private String className;
private String methodName;
private Double time;
private String message;
private String stackTrace;
private Date createTime;
private Integer delFlag;
}
2. DataiApexFlowCoverage(Flow 覆盖率实体)- 新建
@Data
public class DataiApexFlowCoverage {
private Long id;
private Long testResultId;
private String name;
private String type;
private String namespace;
private Integer numElements;
private Integer numElementsCovered;
private Double coveragePercent;
private Date createTime;
private Integer delFlag;
}
接口设计
RESTful API 设计
1. 运行所有测试
- 接口名称:runAllTests
- 请求方式:POST
- 请求路径:
/api/apex/test/run-all - 权限要求:
@PreAuthorize("@ss.hasPermi('apex:test:execute')") - 请求参数:
skipCodeCoverage(Boolean, 可选):是否跳过代码覆盖率检查,默认 falsemaxFailedTests(Integer, 可选):最大失败测试数,默认 0(不限制)
- 响应数据:RunTestsResultVo
2. 运行指定类的测试
- 接口名称:runTestsByClasses
- 请求方式:POST
- 请求路径:
/api/apex/test/run-by-classes - 权限要求:
@PreAuthorize("@ss.hasPermi('apex:test:execute')") - 请求参数:
classes(String[], 必填):要测试的类名称数组skipCodeCoverage(Boolean, 可选):是否跳过代码覆盖率检查,默认 falsemaxFailedTests(Integer, 可选):最大失败测试数,默认 0
- 响应数据:RunTestsResultVo
3. 运行指定包的测试
- 接口名称:runTestsByPackages
- 请求方式:POST
- 请求路径:
/api/apex/test/run-by-packages - 权限要求:
@PreAuthorize("@ss.hasPermi('apex:test:execute')") - 请求参数:
namespace(String, 可选):命名空间packages(String[], 必填):要测试的包数组skipCodeCoverage(Boolean, 可选):是否跳过代码覆盖率检查,默认 falsemaxFailedTests(Integer, 可选):最大失败测试数,默认 0
- 响应数据:RunTestsResultVo
4. 运行指定测试方法
- 接口名称:runTestsByMethods
- 请求方式:POST
- 请求路径:
/api/apex/test/run-by-methods - 权限要求:
@PreAuthorize("@ss.hasPermi('apex:test:execute')") - 请求参数:
tests(TestsNodeDto[], 必填):要运行的测试方法数组skipCodeCoverage(Boolean, 可选):是否跳过代码覆盖率检查,默认 falsemaxFailedTests(Integer, 可选):最大失败测试数,默认 0
- 响应数据:RunTestsResultVo
5. 查询测试结果
- 接口名称:queryTestResults
- 请求方式:GET
- 请求路径:
/api/apex/test/results - 权限要求:
@PreAuthorize("@ss.hasPermi('apex:test:query')") - 请求参数:
testRunId(String, 可选):测试运行 IDstartTime(String, 可选):开始时间(yyyy-MM-dd HH:mm:ss)endTime(String, 可选):结束时间(yyyy-MM-dd HH:mm:ss)pageNum(Integer, 可选):页码,默认 1pageSize(Integer, 可选):每页大小,默认 10
- 响应数据:PageResult
6. 查询测试详情
- 接口名称:queryTestDetails
- 请求方式:GET
- 请求路径:
/api/apex/test/details - 权限要求:
@PreAuthorize("@ss.hasPermi('apex:test:query')") - 请求参数:
testResultId(Long, 必填):测试结果 IDtype(String, 可选):类型(success/failure/all),默认 allpageNum(Integer, 可选):页码,默认 1pageSize(Integer, 可选):每页大小,默认 20
- 响应数据:TestDetailsVo(包含 successes 和 failures)
7. 查询代码覆盖率
- 接口名称:queryCodeCoverage
- 请求方式:GET
- 请求路径:
/api/apex/test/coverage - 权限要求:
@PreAuthorize("@ss.hasPermi('apex:test:query')") - 请求参数:
testResultId(Long, 可选):测试结果 IDname(String, 可选):类/触发器名称type(String, 可选):类型(Class/Trigger)minCoverage(Double, 可选):最小覆盖率maxCoverage(Double, 可选):最大覆盖率pageNum(Integer, 可选):页码,默认 1pageSize(Integer, 可选):每页大小,默认 20
- 响应数据:PageResult
8. 查询 Flow 覆盖率
- 接口名称:queryFlowCoverage
- 请求方式:GET
- 请求路径:
/api/apex/test/flow-coverage - 权限要求:
@PreAuthorize("@ss.hasPermi('apex:test:query')") - 请求参数:
testResultId(Long, 可选):测试结果 IDname(String, 可选):Flow 名称type(String, 可选):Flow 类型minCoverage(Double, 可选):最小覆盖率maxCoverage(Double, 可选):最大覆盖率pageNum(Integer, 可选):页码,默认 1pageSize(Integer, 可选):每页大小,默认 20
- 响应数据:PageResult
接口权限设计
| 接口 | 权限标识 | 说明 |
|---|---|---|
| POST /api/apex/test/run-all | apex:test:execute | 执行测试权限 |
| POST /api/apex/test/run-by-classes | apex:test:execute | 执行测试权限 |
| POST /api/apex/test/run-by-packages | apex:test:execute | 执行测试权限 |
| POST /api/apex/test/run-by-methods | apex:test:execute | 执行测试权限 |
| GET /api/apex/test/results | apex:test:query | 查询测试结果权限 |
| GET /api/apex/test/details | apex:test:query | 查询测试详情权限 |
| GET /api/apex/test/coverage | apex:test:query | 查询代码覆盖率权限 |
| GET /api/apex/test/flow-coverage | apex:test:query | 查询 Flow 覆盖率权限 |
实现要点
关键实现逻辑
1. 测试执行流程
@Override
@Transactional(rollbackFor = Exception.class)
public RunTestsResultVo runTests(RunTestsRequestDto dto) throws SalesforceAuthException, SalesforceOperationException {
lock.lock();
try {
// 1. 构建 RunTestsRequest
RunTestsRequest request = buildRunTestsRequest(dto);
// 2. 获取 SoapConnection
SoapConnection connection = apexConnectionFactory.getConnection("source");
// 3. 执行测试
RunTestsResult result = connection.runTests(request);
// 4. 保存测试结果
saveTestResults(result);
// 5. 转换为 VO 并返回
return convertToVo(result);
} catch (SalesforceAuthException e) {
throw e;
} catch (Exception e) {
throw new SalesforceOperationException("运行测试失败: " + e.getMessage(), e);
} finally {
lock.unlock();
}
}
2. 测试结果保存流程
private void saveTestResults(RunTestsResult result) {
// 1. 保存测试结果主表
DataiApexTestResult testResult = new DataiApexTestResult();
testResult.setTestRunId(result.getApexLogId()); // 使用 apexLogId 作为 testRunId
testResult.setNumTestsRun(result.getNumTestsRun());
testResult.setNumFailures(result.getNumFailures());
testResult.setTotalTime(result.getTotalTime());
testResult.setApexLogId(result.getApexLogId());
testResult.setTestTime(new Date());
testResult.setAllPassed(result.getNumFailures() == 0);
// 计算总体覆盖率
double totalCoverage = calculateTotalCoverage(result.getCodeCoverage());
testResult.setTotalCoverage(totalCoverage);
testResultMapper.insert(testResult);
// 2. 保存测试成功详情
saveTestSuccesses(result.getSuccesses(), testResult.getId());
// 3. 保存测试失败详情
saveTestFailures(result.getFailures(), testResult.getId());
// 4. 保存代码覆盖率
saveCodeCoverages(result.getCodeCoverage(), testResult.getId());
// 5. 保存 Flow 覆盖率
saveFlowCoverages(result.getFlowCoverage(), testResult.getId());
}
异常处理
1. 异常分类
| 异常类型 | 说明 | 处理方式 |
|---|---|---|
| SalesforceAuthException | Salesforce 认证失败 | 抛出,由全局异常处理器处理 |
| SalesforceOperationException | Salesforce 操作失败 | 抛出,由全局异常处理器处理 |
| IllegalArgumentException | 参数校验失败 | 抛出,返回 400 错误 |
| Exception | 其他异常 | 转换为 SalesforceOperationException |
2. 异常处理示例
try {
// 执行测试
} catch (ConnectionException e) {
log.error("Salesforce 连接失败: {}", e.getMessage());
throw new SalesforceAuthException("Salesforce 连接失败: " + e.getMessage(), e);
} catch (Exception e) {
log.error("运行测试失败: {}", e.getMessage(), e);
throw new SalesforceOperationException("运行测试失败: " + e.getMessage(), e);
}
性能优化
1. 数据库优化
- 索引优化:为所有查询字段建立索引(test_run_id、test_time、class_name、method_name 等)
- 分页查询:使用分页查询避免大数据量查询导致的性能问题
- 批量插入:使用 MyBatis 的批量插入功能提高插入性能
2. 并发控制
- ReentrantLock:使用 ReentrantLock 保证测试执行的多线程安全
- 连接池:复用 ApexConnectionFactory 的连接池管理
安全设计
1. 权限控制
- 接口级别:使用 @PreAuthorize 进行接口级别的权限控制
- 数据级别:使用 dept_id 进行数据隔离
2. 数据验证
- 参数校验:使用 @Validated 进行参数校验
- SQL 注入防护:使用 MyBatis 的参数化查询