Learning
VOL. VII · NO. 95 · OTC Derivatives · 19 JUL 2026

Testing And Qa

OTC 衍生品 · 19 JUL 2026 · 12 min read · 1,944 words
· · ·

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-aseds-utility 中约 120 个 JUnit 测试类,每个都在 setUp() 中连接真实 Oracle 数据库
  • 定价引擎离线测试eds-price-server 使用一个非标准 src/testcase/ 目录(不是 src/test/java
  • 压力测试这不是传统意义上的性能测试——pkg_stresstest.pks 是一个 PL/SQL 包,测试投资组合在压力情景下的表现,是业务功能的一部分

为什么没有单元测试?

从代码组织中可以看到线索:

  1. 代码耦合度高——hedging-as 是一个巨大的 Maven 模块,所有类都直接访问 IMS 连接和数据库连接,没有依赖注入,很难 mock
  2. 团队文化——在那个年代,“写完代码 → 部署到测试环境 → 人工验证”被认为足够
  3. 工具限制——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-commonpom.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 ActionsPR 检查,使用 PG 服务容器
otc/lens frontendGitHub Actionsnpx vitest run + 覆盖率报告
otc/lens backendGitHub 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

原因可能包括:

  1. 构建时间:运行测试可能使构建从 30 秒变成 3 分钟。对于频繁部署来说,这不可接受
  2. 测试可靠性:单元测试可能因为环境差异而失败(比如连接不上测试数据库),导致部署阻塞
  3. 数据库依赖:大多数测试直接连接真实数据库,CI 环境中没有
  4. 团队节奏:在快速迭代的压力下,“先部署再补测试”变成了”先部署,以后再说”
  5. 没有追究机制:谁都不会因为没写测试被问责,但写了测试可能会延迟上线

质量保障的实际做法

既然自动测试覆盖率低,那质量靠什么?

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 测试来捕获这种"显示不对但逻辑不报错"的问题。

业务影响

风险点

  1. 质量不可测量:没有覆盖率数据,也没有测试结果趋势,管理层无法判断系统质量在变好还是变差
  2. 回归风险高:改了一行代码可能会破坏另一个看似不相关的功能,而无人知晓,直到生产出问题
  3. 新人上手慢:测试代码是最好的文档——没有测试意味着新开发者只能通过阅读生产代码来理解系统
  4. 重构代价大:没有测试保障,重构就像换飞机引擎,没人敢动
  5. 合规审计风险:金融监管对系统质量控制有越来越高的要求,没有自动测试的未来可能成为合规难点

有利因素

  1. 核心系统运行时间长:eds-web-app 等系统已运行 8+ 年,大部分 bug 已被时间”磨平”
  2. 团队经验丰富:开发和运维人员对系统非常熟悉,能从错误堆栈中快速定位问题
  3. 快速回滚能力:容器化部署使得回滚只需要几秒钟

数据目录

测试框架依赖(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