关于 modernize-test-starter
面向 UI5 开发者的技能,用于将单元测试和 OPA5 测试迁移到 Test Starter。涵盖旧测试结构检测、文件与配置调整及迁移验证。
主要功能
- 01检测单元测试和 OPA 测试模式
- 02生成 Test Starter 套件配置
- 03转换单元测试和 OPA journey
- 04处理有条件的 iframe 迁移
- 05检查迁移结构与遗留文件
适用场景
- 01现代化 UI5 项目的旧测试配置
- 02将 OPA5 启动器迁移至 Test Starter
- 03查找测试转换后的遗留文件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ui5/ plugins-coding-agents --skill modernize-test-starter安装后在 Claude Code 中输入/modernize-test-starter确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/linter-invisible-patterns.mdpattern-a-modernization.mdpattern-b-modernization.mdpattern-u-iframe-migration.mdtestsuite-configuration.mdscripts/check-dangling-entries.jscheck-dangling-entries.test.jsdetect-cross-window-imports.jsdetect-cross-window-imports.test.jsparse-testsuite.jsparse-testsuite.test.js详细介绍
迁移到 Test Starter
此技能将 UI5 项目的整个测试基础架构(包括单元测试和 OPA5 集成测试)迁移到 Test Starter 概念。
参考:博客文章:简化测试设置
为什么要迁移
Test Starter 用一个通用测试页面和声明式 testsuite.qunit.js 配置,取代了每个测试各自的 HTML 引导。其优点包括:消除样板 HTML/JS、自动处理 QUnit/Sinon/覆盖率设置、确保符合 CSP、提供一致的配置、支持每个测试隔离运行和并行执行。
绝不要跳过 Test Starter 迁移
OPA5 测试 HTML 文件始终可以转换——它们遵循固定模式。无效的借口包括:“复杂的模拟服务器”(位于宿主中,而非入口点)、“对 sap.ui.define 进行猴子补丁”(同上)、“FLP 沙盒引导”(同上)、“文件太多”(批量处理)、“过于复杂”(无效)。
| 类型 | 示例 | 操作 |
|---|---|---|
| 测试驱动程序 | opa/<Area>/<TestName>.qunit.html | 转换为 Test Starter |
| 测试宿主 | test<ServiceName>.html | 保持原样(被测应用位于 iframe 中) |
| 测试套件 | testsuite.qunit.html | 替换为 testsuite.qunit.js |
| 开发沙盒 | flpSandbox.html | 忽略 |
唯一真正的技术挑战:OPA5 入口点可能使用 jQuery.sap.require() 加载 QUnit 引导库——请替换为 sap.ui.require,或注明 Test Starter 会自动加载这些库。
前提条件
读取 manifest.json → sap.app/id。这是你的命名空间:
<NAMESPACE>— 将点替换为斜杠(例如,my.app→my/app)<NAMESPACE-WITH-DOTS>— 带点的原始值
阶段 0:检测
0.1 检查单元测试
查找包含 unitTests.qunit.html、unitTests.qunit.js、allTests.js、AllTests.js 或 allTests.qunit.js 的 webapp/test/unit/。识别所有旧版聚合文件——这类 JS 文件的唯一用途是通过 sap.ui.require/sap.ui.define 加载其他测试模块,且不包含实际测试逻辑(QUnit.module、QUnit.test、assert.*)。这些文件的内容将内联到 unitTests.qunit.js 中,然后删除这些文件。
0.2 对 OPA 启动器和 FLP 沙盒进行分类
当 needsIframeMigration = (launcher === "in-window" AND flpSandbox === true) 时,才执行阶段 5b。
node <skill-dir>/scripts/parse-testsuite.js --detect-launcher webapp/test > /tmp/launcher.json
返回 { "launcher": "iframe"|"in-window"|"mixed"|"none", "flpSandbox": true|false, "needsIframeMigration": true|false }。
launcher | flpSandbox | 操作 |
|---|---|---|
iframe | 任意 | 模式 I — 仅执行阶段 5,跳过 5b |
in-window | true | 模式 U — 执行阶段 5 和阶段 5b |
in-window | false | 普通的窗口内模式 — 跳过 5b,保留 iStartMyUIComponent |
mixed | 任意 | 暂停。 记录到 MODERNIZATION_ISSUES.md,请开发者协调 |
none | 任意 | 没有 OPA 测试 — 跳过 OPA 阶段 |
不支持模式 U + 模式 B。 如果 needsIframeMigration === true 且阶段 0.3 报告模式 B,请暂停并告知开发者。
0.3 检查 OPA 测试 — 识别模式
模式 A — “单个 HTML + AllJourneys”:opaTests.qunit.html + AllJourneys.js 编排器。
模式 B — “多个独立 HTML 文件”:webapp/test/opa/ 下的多个 *.qunit.html。
检测:
find webapp/test -name "AllJourneys.js" -o -name "AllJourneys.json"
find webapp/test/opa -name "*.qunit.html" -type f 2>/dev/null | wc -l
0.4 运行解析脚本
node <skill-dir>/scripts/parse-testsuite.js <testsuite.qunit.html> <test-base-dir> <namespace>
输出:pattern(A/B)、summary(计数)、entries(模块→配置映射)、opaConfig(模式 A 使用)。请保存这些输出——后续操作都将以此为依据。
0.5 报告引导覆盖
node <skill-dir>/scripts/parse-testsuite.js --scan-bootstrap-overrides webapp/test > /tmp/bootstrap-overrides.json
检测内容:sap.ui.define = ...、sap.ui.require = ...、defineModuleSync(...)。将每项发现附加到 MODERNIZATION_ISSUES.md。不要重写覆盖项——交付内容是报告。
阶段 1:创建 testsuite.qunit.js(主套件)
列出所有测试:通过 "unit/unitTests" 委托单元测试,并逐一列出 OPA 测试流程。根据解析脚本的输出构建 OPA 条目——键为 webapp/test/ 下的相对路径,不含 .qunit.js。
sap.ui.define(function() {
"use strict";
return {
name: "QUnit test suite for <NAMESPACE-WITH-DOTS>",
defaults: {
page: "ui5://test-resources/<NAMESPACE>/Test.qunit.html?testsuite={suite}&test={name}",
qunit: { version: 2 },
sinon: { version: 4 },
ui5: { theme: "sap_horizon" },
loader: {
map: { "*": {
"sap/ui/thirdparty/sinon": "sap/ui/thirdparty/sinon-4",
"sap/ui/thirdparty/sinon-qunit": "sap/ui/qunit/sinon-qunit-bridge"
}},
paths: { "<NAMESPACE>": "../" }
},
coverage: { only: ["<NAMESPACE>"], never: ["<NAMESPACE>/test"] }
},
tests: {
// ----- Unit Tests -----
"unit/unitTests": { title: "Unit Tests" },
// ----- OPA Integration Tests -----
"integration/NavigationJourney": { title: "Navigation Journey" }
}
};
});
请阅读 references/testsuite-configuration.md,了解选项的详细说明。
要点:
page必须使用ui5://协议前缀- 不需要
module属性——Test Starter 会为条目键追加.qunit {suite}和{name}占位符为必填项
附加加载器路径
必须执行。 从所有测试 HTML 文件中提取全部资源根映射:
grep -rh "data-sap-ui-resourceroots" webapp/test/ --include="*.html"
将点号分隔的键转换为斜杠分隔形式,并添加到 loader.paths。所有值都相对于 Test.qunit.html(位于 webapp/test/)解析——据此重新计算子目录 HTML 文件中的路径。
如果两个 HTML 文件为同一个键定义了不同的值,优先使用主 testsuite.qunit.html/opaTests.qunit.html 中的值。少数测试需使用各自的 loader.paths 覆盖项。
阶段 2:创建 Test.qunit.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="../resources/sap/ui/test/starter/runTest.js"
data-sap-ui-resource-roots='{ "test-resources.<NAMESPACE-WITH-DOTS>": "./" }'
></script>
</head>
<body class="sapUiBody">
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>
阶段 3:更新 testsuite.qunit.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>QUnit test suite for <NAMESPACE-WITH-DOTS></title>
<script src="../resources/sap/ui/test/starter/createSuite.js"
data-sap-ui-testsuite="test-resources/<NAMESPACE>/testsuite.qunit"
data-sap-ui-resource-roots='{ "test-resources.<NAMESPACE-WITH-DOTS>": "./" }'
></script>
</head>
<body></body>
</html>
阶段 4:现代化单元测试 JS 文件
4.0 首先——删除冗余聚合器
冗余聚合器不包含任何实际测试逻辑——只有 sap.ui.require([deps], function() { QUnit.start(); }) 或仅用于加载的 sap.ui.define。QUnit.config.autostart = false 和 QUnit.start() 属于引导代码,不是测试逻辑。
操作:记下它们加载的测试模块(供 unitTests.qunit.js 使用),然后立即删除 .js 文件及其配套的 .html 文件。
4.1 转换并重命名实际测试文件
包含实际测试逻辑(QUnit.module、QUnit.test、assert.*)的文件需要:
- 重命名,将后缀改为
.qunit.js(例如,App.controller.js→App.controller.qunit.js) - 转换为
sap.ui.define(移除QUnit.config.autostart和Core.ready包装)
// Before (App.controller.js)
QUnit.config.autostart = false;
sap.ui.getCore().attachInit(function() {
sap.ui.require(["my/app/model/formatter"], function(formatter) {
QUnit.module("formatter");
QUnit.test("formatValue", function(assert) { ... });
});
});
// After (App.controller.qunit.js)
sap.ui.define(["my/app/model/formatter"], function(formatter) {
"use strict";
QUnit.module("formatter");
QUnit.test("formatValue", function(assert) { ... });
});
4.2 创建 unitTests.qunit.js 聚合器
直接列出所有实际单元测试模块(来自已删除的聚合器及其他 .qunit.js 文件):
sap.ui.define([
"./controller/Main.qunit",
"./model/formatter.qunit"
]);
规则:使用相对 ./ 路径,包含 .qunit 后缀(不含 .js)。
jsUnitTestSuite 转换
如果旧的 testsuite.qunit.js 使用了 jsUnitTestSuite,则它已在阶段 1 中替换。删除旧内容。
阶段 5:现代化 OPA 测试
模式 A——单个 HTML + AllJourneys
请阅读 references/pattern-a-modernization.md,了解详细说明。
- 根据 AllJourneys.js 创建 OpaSetup.js——提取
Opa5.extendConfig和页面对象导入。OpaSetup 不得导入sap/ui/test/opaQunit。 - 将测试流程文件重命名为
.qunit.js - 更新测试流程——将
"./OpaSetup"添加为副作用依赖项(相对路径) - 处理 autoWait 覆盖项——按测试流程设置
Opa5.extendConfig - 在 OpaSetup.js 中保留 testLibs 配置
// OpaSetup.js (no opaQunit!)
sap.ui.define(["sap/ui/test/Opa5", "test-resources/<NAMESPACE>/integration/pages/App"
], function(Opa5) {
"use strict";
Opa5.extendConfig({ viewNamespace: "<APP-NAMESPACE>.view.", autoWait: true });
});
// Journey file (opaQunit here, OpaSetup relative)
sap.ui.define(["sap/ui/test/opaQunit", "sap/ui/test/Opa5", "./OpaSetup"
], function(opaTest, Opa5) {
"use strict";
// opaTest(...) calls
});
模式 B——多个独立 HTML 文件
请阅读 references/pattern-b-modernization.md,了解详细说明。
- 清点调用
Opa5.createPageObjects的工具模块 - 创建 OpaSetup.js——整合工具模块导入和
Opa5.extendConfig - 将测试流程重命名为
.qunit.js,并添加 OpaSetup 依赖项 - 处理解析脚本
autoWaitFalseFiles中的 autoWait 覆盖项 - 多模块 HTML 文件——每个模块生成一个 testsuite 条目(绝不要自行编造
*Combined名称)。参见references/pattern-b-modernization.md第 6 步。
阶段 5b:将窗口内 OPA 迁移到纯 Component iframe
仅当阶段 0.2 报告 needsIframeMigration: true 时运行。
阅读 references/pattern-u-iframe-migration.md 获取完整说明。摘要:
- 创建
opaIframe.qunit.html+opaIframeBoot.js— 纯 Component iframe。HTML 加载sap/ushellsandbox.js 以提供存根,不包含sap-ushell-config。Boot 模块运行mockserver.init()→new ComponentContainer(...).placeAt("content")。符合 CSP 要求:使用data-sap-ui-oninit="module:...",不包含内联脚本。 - 重写
arrangements/Common.js— 将iStartMyApp改为iStartMyAppInAFrame(...)。移除 mockserver 导入,移除_clearSharedData。 - 重写 journeys — 将
iStartMyUIComponent({...})改为iStartMyApp()(转发 hash/autoWait)。 - 从
OpaSetup.js移除父 frame 中的 mockserver 初始化 — 现在由 iframe 启动。 - 跨窗口实例化控件 —
waitFor回调中的 UI5 控件必须通过Opa5.getWindow().sap.ui.require(...)解析。OPA 安全依赖保留在父窗口中:sap/ui/test/*、sap/ui/core/routing/History。检查:node <skill-dir>/scripts/detect-cross-window-imports.js <project-root>。 - 跨窗口 jQuery/DOM — 将裸用的
$(...)、document.*、window.*替换为Opa5.getJQuery()(...)、Opa5.getWindow().*。 - 路由辅助函数 — 使用纯 Component-router hash,不要添加
#app-tile&/前缀。 - Mockserver
sap-message信封 — POST 处理程序需要sap-messageheader,供消息收集器使用。 - ErrorHandler 空值保护 — 对
getElementsByTagName("message")[0].firstChild.data添加空值保护。 - 不要在
loader.paths中注册<NAMESPACE>/test/integration/opaIframe。
第 5–9 项因项目而异;根据参考文件中的检测 grep 逐项应用。
阶段 6:删除旧文件
单元测试文件
- 删除
unitTests.qunit.html、legacyTests.qunit.html(旧版引导文件) - 确认阶段 4.0 中所有多余的聚合文件均已删除
OPA 文件 — 模式 A
- 删除
opaTests.qunit.html、AllJourneys.js、AllJourneys.json
OPA 文件 — 模式 U(仅当 needsIframeMigration 时)
- 如果之前的尝试留下了
flpSandbox.qunit.html,则将其删除 - 确认没有 journey 仍在调用
iStartMyUIComponent
OPA 文件 — 模式 B
- 删除
webapp/test/opa/下所有*.qunit.html(数量必须与summary.totalActive一致)
不要删除
testsuite.qunit.html(已更新)、Test.qunit.html(已创建)
阶段 7:验证
- 数量检查:
testsuite.qunit.js中的 OPA 条目数与解析脚本统计的 OPA 总数一致。 - 悬空条目检查:运行
node <skill-dir>/scripts/check-dangling-entries.js webapp/test— 退出码为 0 表示正常。 - Linter:运行
npx @ui5/linter— 不再有prefer-test-starter警告。 - 结构审查:
Test.qunit.html→runTest.js;testsuite.qunit.html→createSuite.js;testsuite.qunit.js→ 单元测试 delegate + OPA journeys;所有单元测试文件均使用sap.ui.define;存在OpaSetup.js;每个 journey 均导入该文件;没有过时的 HTML 文件。 - 模式 U 验证(如果
needsIframeMigration):- 存在
opaIframe.qunit.html+opaIframeBoot.js;没有内联<script>内容;没有window["sap-ushell-config"] grep -rn "iStartMyUIComponent\b" webapp/test→ 命中数为 0grep -rn "#app-tile&/" webapp/test→ 命中数为 0mockserver.init()只出现在opaIframeBoot.js中- 没有指向 opaIframe/flpSandbox 的
loader.paths别名 detect-cross-window-imports.js退出码为 0- ErrorHandler 的 XML 解析已添加空值保护,或已标记
- 存在
示例
示例 A — 模式 A(单个 HTML + AllJourneys)
命名空间:com.mycompany.myapp,4 个 OPA journey + 2 个单元测试。
完成后:testsuite.qunit.js → 5 个条目(1 个单元测试 + 4 个 OPA)。AllJourneys.js → OpaSetup.js + 各个条目。已删除:AllJourneys.json、opaTests.qunit.html、unitTests.qunit.html。
示例 B — 模式 B(多个独立 HTML 文件)
命名空间:com.mycompany.myapp,45 个 OPA 测试流程 + 3 个单元测试。
完成后:testsuite.qunit.js → 46 个条目(1 个单元测试 + 45 个 OPA 测试流程)。OpaSetup.js = 工具导入的并集。3 个测试流程覆盖设置为 autoWait: false。已删除:46 个 HTML 文件 + unitTests.qunit.html。
重要说明
runTest.js与createSuite.js:createSuite.js= 测试套件概览。runTest.js= 单个测试的Test.qunit.html。.qunit.js后缀规则:只有测试套件入口键需要.qunit.js。通过sap.ui.define加载的依赖项(页面对象、OpaSetup.js)保留普通.js。- 依赖路径中的
.qunit:"./FilterBar.qunit"会解析为FilterBar.qunit.js。不带后缀 → 找不到文件。 test-resources/前缀:webapp/test/下的依赖项使用test-resources/<NAMESPACE>/...,而不是<NAMESPACE>/test/...。- 转换现有的
<NAMESPACE>/test/依赖项:第 5 阶段之后,扫描所有测试.js文件,并转换为test-resources/<NAMESPACE>/。 - 相对路径与
test-resources/:同一目录 →./。跨目录 →test-resources/<NAMESPACE>/...。 - OpaSetup.js 不得导入
sap/ui/test/opaQunit— 该导入应放在每个测试流程文件中。 - 副作用导入放在依赖数组末尾 — 前置会导致参数位置偏移。
完成检查清单
| # | 检查项 |
|---|---|
| 1 | Test.qunit.html 存在且包含 runTest.js |
| 2 | testsuite.qunit.html 使用 createSuite.js |
| 3 | testsuite.qunit.js 包含 "unit/unitTests" + 所有 OPA 测试流程 |
| 4 | 不再有多余的聚合器 |
| 5 | 不再有过时的测试 HTML 引导文件 |
| 6 | unitTests.qunit.js 仅引用实际存在的测试文件 |
| 7 | OPA 入口数量与解析脚本的总数一致 |
| 8 | 悬空入口检查通过(退出码为 0) |
| 9 | 在 MODERNIZATION_ISSUES.md 中报告引导覆盖项(如有) |
| 10 | 启动器 + FLP 已分类;遇到 mixed 时已停止 |
| 11 | 若 needsIframeMigration:iframe 文件存在,无内联脚本,无 iStartMyUIComponent |
| 12 | 若 needsIframeMigration:无 FLP 哈希前缀,无加载器别名 |
| 13 | 若 needsIframeMigration:ErrorHandler 已设防护或已标记 |
| 14 | 若 needsIframeMigration:detect-cross-window-imports.js 以退出码 0 结束 |
相关技能
- fix-csp-compliance — 旧 HTML 内联脚本违反 CSP;Test Starter 会将其移除。
- fix-linter-blind-spots — 捕获 linter 遗漏的运行时破坏性模式。
- fix-js-globals (cases 1b/1c) — 处理 linter 报告的
sap.*全局变量。