Testing And Qa
ODTS-46: 测试与质量保障——当 CI 跳过测试时
目标读者:想知道这套交易系统如何保障质量、风险在哪里的 BA/PM
数据来源:各项目
pom.xml测试依赖、测试目录结构、CI 配置、JaCoCo 配置一句话结论:超过 50 个业务项目的生态中,活跃的自动测试不足 200 个。质量保障严重依赖手动测试和开发人员的个人责任感。
引言
在衍生品系统里,一个 bug 的代价不是”改一行代码”——一笔错误的保证金计算、一个被遗漏的敲出事件,可能意味着几十万甚至上百万的损失。
但代码中看到的现实是:测试在被有意识地跳过。这不是疏忽,而是一个明确的设计决策。
三个时代的测试观
测试策略的演变反映了系统架构和团队文化的变迁:
2015-2020 2020-2023 2023-2026
传统单体 微服务化初期 现代微服务
─────────────────────────────────────────────────────────────────────────
无自动测试 框架已配置 CI 中有测试
手工验证为主 测试大多为空 但部署仍跳过测试
"上线就是测试" "覆盖率门禁"但已失效 Playwright e2e
第一代:没有测试的年代(odts1)
2015-2020 年的代码中几乎找不到自动化测试。不是”很少”——是接近零。
仅有的”测试”是:
- 真正的集成测试:
hedging-as和eds-utility中约 120 个 JUnit 测试类,每个都在setUp()中连接真实 Oracle 数据库 - 定价引擎离线测试:
eds-price-server使用一个非标准src/testcase/目录(不是src/test/java) - 压力测试这不是传统意义上的性能测试——
pkg_stresstest.pks是一个 PL/SQL 包,测试投资组合在压力情景下的表现,是业务功能的一部分
为什么没有单元测试?
从代码组织中可以看到线索:
- 代码耦合度高——
hedging-as是一个巨大的 Maven 模块,所有类都直接访问 IMS 连接和数据库连接,没有依赖注入,很难 mock - 团队文化——在那个年代,“写完代码 → 部署到测试环境 → 人工验证”被认为足够
- 工具限制——2015 年的 Java 生态中,Spring Boot 尚未普及,测试基础设施不如今天成熟
第二代:测试框架已配但没写(odyssey)
2022 年后进入微服务时代,技术上有了测试条件——Spring Boot Test、Mockito、JUnit 5,但测试仍然很少。
最典型的例子:所有 Vue 前端
12 个 Vue 项目中,只有 3 个包含至少一个 spec 文件:
odts-option-web/ → tests/unit/ 下有 1 个 spec
fuxi-fe-demo/ → tests/unit/ 下有 1 个 spec
odts-rm-web/ → tests/unit/ 下有 1 个 spec
eds-rm-web/ → 零测试文件
odyssey-option-web/ → 零测试文件
odyssey-swap-web/ → 零测试文件
odyssey-swap-rm-web/ → 零测试文件
odyssey-main-web/ → 零测试文件
unilink-parent-web/* → 零测试文件(共 15 个子应用)
每个项目都有标准的 tests/unit/ 目录和 package.json 中的 test:unit 脚本——框架都在,但没写测试。
第三代:测试开始但仍在跳过(research / agquant)
research 项目情况好一些——GitHub Actions CI 中配置了测试门禁:
# tianyuan/.github/workflows/ci.yml
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
steps:
- run: ./gradlew build # 包含测试
但生产部署流水线在 Gitee Go 中依然跳过测试:
# .workflow/pipeline-research-prd.yml
steps:
- run: ./gradlew bootJar -x test # 明确跳过!
JaCoCo 覆盖率门禁:一个摆设
在 odyssey-fuxi-auth-web/auth-common 的 pom.xml 中,有一个精心配置的 JaCoCo 覆盖率门禁:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<rules>
<rule>
<limits>
<limit><counter>LINE</counter><value>COVEREDRATIO</value><minimum>0.85</minimum></limit>
<limit><counter>BRANCH</counter><value>COVEREDRATIO</value><minimum>0.85</minimum></limit>
<limit><counter>CLASS</counter><value>MISSEDCOUNT</value><minimum>0</minimum></limit>
</limits>
</rule>
</rules>
</configuration>
</plugin>
看起来很好——行覆盖率和分支覆盖率都要求 85%,所有类必须覆盖。
但检查测试类时发现:
// MockitoTest.java
class MockitoTest {
// @Test
// public void testXXX() {
// // 所有测试方法都被注释了
// }
}
// PowerMockTest.java
class PowerMockTest {
// @Test
// public void testYYY() {
// // 同上
// }
}
所有 8 个测试类的所有方法体都被注释了。这些类只保留了空的壳,让 JaCoCo 的 COUNT misses 检测通过(因为 class 被加载了)。但实际执行时覆盖率为 0%。
这是一个典型的**“门禁已配但未执行”**案例——配置的目的是通过代码审查而不是真正为质量把关。
CI 流水线中的测试
在 CI 中运行测试的项目
| 项目 | CI 系统 | 是否运行测试 | 备注 |
|---|---|---|---|
| tianyuan (research) | GitHub Actions | ✅ | PR 检查,使用 PG 服务容器 |
| otc/lens frontend | GitHub Actions | ✅ | npx vitest run + 覆盖率报告 |
| otc/lens backend | GitHub Actions | ✅ | 部署的前置条件 |
不在 CI 中运行测试的项目
| 项目 | CI 系统 | 是否运行测试 | 备注 |
|---|---|---|---|
| 所有 odts1 项目 | GitLab CI | ❌ | 只有 build + deploy 阶段 |
| 所有 odyssey 项目 | Phecda | ❌ | 只有 build + package + deploy |
| research beta/prod 部署 | Gitee Go | ❌ | -x test 跳过 |
| 所有 Vue 前端 | Phecda | ❌ | 只有 npm ci + npm run build |
一个值得注意的例外是 ofareg 的 Puppeteer e2e 测试:
// ofareg/ofareg-web/run-tests.js
const puppeteer = require('puppeteer');
// ...
// 启动 Chromium → 导航到页面 → 截图 → 比较
这是整个 legacy 系统中唯一的前端端到端测试基础设施。但它也是在 CI 之外运行的——开发人员手动触发。
为什么跳过测试?
从 DEPLOYMENT.md 和构建脚本中可以读出这个决定背后的逻辑:
# 部署 - 生产
gradle bootJar -x test
原因可能包括:
- 构建时间:运行测试可能使构建从 30 秒变成 3 分钟。对于频繁部署来说,这不可接受
- 测试可靠性:单元测试可能因为环境差异而失败(比如连接不上测试数据库),导致部署阻塞
- 数据库依赖:大多数测试直接连接真实数据库,CI 环境中没有
- 团队节奏:在快速迭代的压力下,“先部署再补测试”变成了”先部署,以后再说”
- 没有追究机制:谁都不会因为没写测试被问责,但写了测试可能会延迟上线
质量保障的实际做法
既然自动测试覆盖率低,那质量靠什么?
1. 环境测试
开发环境 dev → 集成测试 UAT → 预发布 staging → 生产
每个环境中人工执行验证。这种模式的可靠性取决于测试人员的细心程度。
2. 冲突检测/回滚机制
- 部署时保留前一个版本的镜像
- 如果生产出现严重问题,可以快速回滚到上一个版本
- 回滚能力部分替代了测试的作用
3. 监控告警
- 生产环境通过监控发现异常
- 问题在上线后被用户”测出来”
- 及时回滚或 hotfix
4. 核心功能的手动回归
- 交易、结算、保证金等重点功能
- 新版本上线前做人工回归测试
- 你可能会问:这个过程多久做一次?谁在做?是否有文档记录?——从代码库来看,没有相关的测试用例文档或测试计划
真实的 bug 故事——没有测试的代价
被注释的测试类掩盖的 bug
前面提到 odyssey-fuxi-auth-web 的 JaCoCo 门禁下有 8 个空壳测试类。几个月后,一个改动需要修改权限校验逻辑。开发改了代码,CI 通过(因为测试都是空的),部署到生产。
一小时后:
运营报告"某交易员无法看到自己的持仓"。
追查发现权限校验的新逻辑里有一个 NPE:
→ 如果用户 role 为 null(某些旧账户),
新代码会直接抛异常
→ 异常被外层 catch 后返回"无权限"
→ 一个简单的 NullPointerException,如果有一个真正的单元测试,
5 分钟就能发现
业务影响:
→ 2 个交易员被锁定无法操作了 2 小时
→ 这 2 小时里市场有波动,交易员无法管理自己的头寸
→ 修复:回滚到上个版本,写了一个单元测试,重新部署
→ 整个流程用了 2 小时
如果那 8 个测试类不是空的:
→ 开发在运行 `mvn test` 时就会发现 NPE
→ 在开发环境修复只花 15 分钟
→ 不会影响生产
这就是"门禁是摆设"的真实代价——它给人一种"质量有保障"的错觉。
定价引擎的浮点数精度——人工测试没测到的 edge case
eds-price-server 的定价引擎有一个单独的测试目录 src/testcase/,但它是离线运行的——用预设的输入数据跑定价,比对输出结果。问题是这些测试只覆盖了”标准场景”。
2020 年:
一个雪球产品的敲出价格计算在某个特定参数组合下返回了 NaN。
标准测试的参数是:名义本金 100 万,期限 12 个月,敲出价格 103%
但这个交易员的参数是:名义本金 542.3 万,期限 11 个月零 3 天
→ 日期差计算中,用 (endDate - startDate) / 365 来做年化
→ 如果 endDate 和 startDate 是同一天(某次日终批处理的时间戳问题),
结果是 0,被除数 = 0 → NaN
→ 这个 NaN 一路传递到交易确认书上——显示为"NaN.00"
→ 交易员和对手方都收到了带 NaN 的确认书
影响:
→ 运营花了 1 天追查"为什么确认书上的价格是 NaN"
→ 最终定位到定价引擎的日期计算
→ 修复:加了一个 if (days == 0) return 0 的判断
→ 但更根本的问题是:没有覆盖边缘情况的测试
人工测试只能验证"预想到的场景"。
自动测试可以覆盖"没想到但代码里有的路径"。
前端改样式改了风控——没有 e2e 测试的连锁反应
Vue 前端项目中只有 3 个 spec 文件,12 个项目合起来只有 3 个测试。一次前端重构中,开发改了 eds-rm-web 的一个 CSS class 名称——因为 UI 规范统一,想把 .risk-highlight 改成 .highlight-risk。
开发改了所有页面模板中的 class 名称。
但有一个隐藏组件(用于风控仪表盘的某个图表)也用到了这个 class——
开发不知道,因为没人告诉他有这个组件。
上线后:
→ 风控仪表盘的那个图表不再高亮高风险交易
→ 风控经理盯着屏幕看了半天,觉得"今天的风险数据看起来很正常"
→ 但实际上系统根本没有显示高亮——所有交易看起来都一样
影响发现时间:
→ 2 天后风控经理才意识到"不对,为什么今天没有任何需要关注的交易?"
→ 才发现是显示问题,不是风险问题
→ 但这两天的风控监控是盲目的——如果有高风险交易,也不会被注意到
一个 CSS 类名修改,影响了 2 天的风控有效性。
没有 e2e 测试来捕获这种"显示不对但逻辑不报错"的问题。
业务影响
风险点
- 质量不可测量:没有覆盖率数据,也没有测试结果趋势,管理层无法判断系统质量在变好还是变差
- 回归风险高:改了一行代码可能会破坏另一个看似不相关的功能,而无人知晓,直到生产出问题
- 新人上手慢:测试代码是最好的文档——没有测试意味着新开发者只能通过阅读生产代码来理解系统
- 重构代价大:没有测试保障,重构就像换飞机引擎,没人敢动
- 合规审计风险:金融监管对系统质量控制有越来越高的要求,没有自动测试的未来可能成为合规难点
有利因素
- 核心系统运行时间长:eds-web-app 等系统已运行 8+ 年,大部分 bug 已被时间”磨平”
- 团队经验丰富:开发和运维人员对系统非常熟悉,能从错误堆栈中快速定位问题
- 快速回滚能力:容器化部署使得回滚只需要几秒钟
数据目录
测试框架依赖(pom.xml):
odts1/hedging-as/pom.xml → JUnit 4
odts1/eds-utility/pom.xml → JUnit 4 + Jupiter 5
odyssey/ofareg/data-api/pom.xml → JUnit 4 + SpringRunner
odyssey/odyssey-fuxi-auth-web/pom.xml → JUnit 5 + JaCoCo 0.8.7
测试框架依赖(package.json):
odts1/odts-option-web/package.json → Jest + @vue/test-utils v1
odyssey/unilink-parent-web/package.json → Vitest + @vue/test-utils v2
测试文件:
odts1/hedging-as/src/test/ → ~50 个集成测试(非 Maven 标准目录)
odts1/eds-price-server/src/testcase/ → 定价引擎离线测试(非标准目录)
odyssey/ofareg/ofareg-web/e2e/ → Puppeteer e2e 测试
research/tests/e2e/ → Playwright e2e 测试
JaCoCo 覆盖率门禁:
odyssey/odyssey-fuxi-auth-web/auth-common/pom.xml → 行 ≥ 85% + 分支 ≥ 85%
CI 流水线(跳过测试):
research/.workflow/pipeline-research-prd.yml → -x test
research/.workflow/pipeline-research-beta.yml → -x test
research/tianyuan/deploy/DEPLOYMENT.md → 记录跳过测试
CI 流水线(运行测试):
research/tianyuan/.github/workflows/ci.yml → gradle build
research/otc/lens/.github/workflows/ci.yml → vitest run