Modernize to Test Starter
This skill modernizes a UI5 project's entire test infrastructure — both unit tests and OPA5 integration tests — to the Test Starter concept.
Reference: Blog post: Simplify Your Test Setup
Why Modernize
The Test Starter replaces per-test HTML bootstrapping with a single generic test page and a declarative testsuite.qunit.js configuration. Benefits: eliminates boilerplate HTML/JS, handles QUnit/Sinon/coverage setup automatically, ensures CSP compliance, provides consistent configuration, enables per-test isolation and parallel execution.
NEVER Skip Test Starter Modernization
OPA5 test HTML files are ALWAYS convertible — they follow a mechanical pattern. Invalid excuses: "complex mock server" (lives in host, not entry point), "sap.ui.define monkey-patching" (same), "FLP sandbox bootstrap" (same), "too many files" (batch-process), "too complex" (invalid).
| Type | Example | Action |
|---|---|---|
| Test Driver | opa/<Area>/<TestName>.qunit.html |
CONVERT to Test Starter |
| Test Host | test<ServiceName>.html |
KEEP AS-IS (app-under-test in iframe) |
| Test Suite | testsuite.qunit.html |
REPLACE with testsuite.qunit.js |
| Dev Sandbox | flpSandbox.html |
IGNORE |
The only real technical challenge: OPA5 entry points may use jQuery.sap.require() for QUnit bootstrap libs — replace with sap.ui.require or note that Test Starter loads them automatically.
Pre-requisites
Read manifest.json → sap.app/id. This is your namespace:
<NAMESPACE>— dots → slashes (e.g.,my.app→my/app)<NAMESPACE-WITH-DOTS>— raw value with dots
Phase 0: Detection
0.1 Check for unit tests
Look for webapp/test/unit/ containing unitTests.qunit.html, unitTests.qunit.js, allTests.js, AllTests.js, or allTests.qunit.js. Identify all legacy aggregator files — JS files whose only purpose is loading other test modules via sap.ui.require/sap.ui.define with no actual test logic (QUnit.module, QUnit.test, assert.*). Their contents will be inlined into unitTests.qunit.js and the files deleted.
0.2 Classify OPA launcher and FLP sandbox presence
Phase 5b is gated on needsIframeMigration = (launcher === "in-window" AND flpSandbox === true).
node <skill-dir>/scripts/parse-testsuite.js --detect-launcher webapp/test > /tmp/launcher.json
Returns { "launcher": "iframe"|"in-window"|"mixed"|"none", "flpSandbox": true|false, "needsIframeMigration": true|false }.
launcher |
flpSandbox |
Action |
|---|---|---|
iframe |
any | Pattern I — Phase 5 only, skip 5b |
in-window |
true |
Pattern U — Phase 5 + Phase 5b |
in-window |
false |
Plain in-window — skip 5b, leave iStartMyUIComponent |
mixed |
any | Halt. Log to MODERNIZATION_ISSUES.md, ask developer to reconcile |
none |
any | No OPA tests — skip OPA phases |
Pattern U + Pattern B is unsupported. If needsIframeMigration === true and Phase 0.3 reports Pattern B, halt and surface to the developer.
0.3 Check for OPA tests — identify pattern
Pattern A — "Single HTML + AllJourneys": opaTests.qunit.html + AllJourneys.js orchestrator.
Pattern B — "Many Individual HTML Files": Multiple *.qunit.html under webapp/test/opa/.
Detection:
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 Run the parse script
node <skill-dir>/scripts/parse-testsuite.js <testsuite.qunit.html> <test-base-dir> <namespace>
Outputs: pattern (A/B), summary (counts), entries (module→config mapping), opaConfig (for Pattern A). Save this — it drives the rest.
0.5 Report bootstrap overrides
node <skill-dir>/scripts/parse-testsuite.js --scan-bootstrap-overrides webapp/test > /tmp/bootstrap-overrides.json
Detects: sap.ui.define = ..., sap.ui.require = ..., defineModuleSync(...). Append each finding to MODERNIZATION_ISSUES.md. Do NOT rewrite overrides — reporting is the deliverable.
Phase 1: Create testsuite.qunit.js (Main)
Lists all tests: unit tests delegated via "unit/unitTests", OPA journeys listed individually. Build OPA entries from parse script output — key = relative path from webapp/test/ without .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" }
}
};
});
Read references/testsuite-configuration.md for detailed option explanations.
Key points:
pageMUST useui5://protocol prefix- No
moduleproperty needed — Test Starter appends.qunitto entry keys {suite}and{name}placeholders are mandatory
Additional loader paths
MANDATORY. Extract ALL resource root mappings from ALL test HTML files:
grep -rh "data-sap-ui-resourceroots" webapp/test/ --include="*.html"
Convert dot-notation keys to slash-notation, add to loader.paths. All values resolve relative to Test.qunit.html (at webapp/test/) — recompute paths from subdirectory HTMLs accordingly.
If two HTMLs define the same key with different values, prefer the main testsuite.qunit.html/opaTests.qunit.html value. Minority tests get per-test loader.paths overrides.
Phase 2: Create 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>
Phase 3: Update 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>
Phase 4: Modernize Unit Test JS Files
4.0 FIRST — Delete redundant aggregators
A redundant aggregator has NO actual test logic — only sap.ui.require([deps], function() { QUnit.start(); }) or load-only sap.ui.define. QUnit.config.autostart = false + QUnit.start() are boot scaffolding, NOT test logic.
Action: Note the test modules they load (for unitTests.qunit.js), then DELETE both .js and companion .html immediately.
4.1 Convert and rename real test files
Files with actual test logic (QUnit.module, QUnit.test, assert.*) need:
- Rename to
.qunit.jssuffix (e.g.,App.controller.js→App.controller.qunit.js) - Convert to
sap.ui.define(removeQUnit.config.autostart,Core.readywrappers)
// 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 Create unitTests.qunit.js aggregator
Directly lists all real unit test modules (from deleted aggregators + any additional .qunit.js files):
sap.ui.define([
"./controller/Main.qunit",
"./model/formatter.qunit"
]);
Rules: relative ./ paths, include .qunit suffix (without .js).
jsUnitTestSuite conversion
If old testsuite.qunit.js used jsUnitTestSuite, it's already replaced in Phase 1. Delete old content.
Phase 5: Modernize OPA Tests
Pattern A — Single HTML + AllJourneys
Read references/pattern-a-modernization.md for detailed instructions.
- Create OpaSetup.js from AllJourneys.js — extract
Opa5.extendConfig+ page object imports. OpaSetup MUST NOT importsap/ui/test/opaQunit. - Rename journey files to
.qunit.js - Update journeys — add
"./OpaSetup"as side-effect dependency (relative path) - Handle autoWait overrides — per-journey
Opa5.extendConfig - Preserve testLibs config in OpaSetup.js
// 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
});
Pattern B — Many Individual HTML Files
Read references/pattern-b-modernization.md for detailed instructions.
- Inventory utility modules calling
Opa5.createPageObjects - Create OpaSetup.js — consolidate utility imports +
Opa5.extendConfig - Rename journeys to
.qunit.js, add OpaSetup dependency - Handle autoWait overrides from parse script's
autoWaitFalseFiles - Multi-module HTML files — emit ONE testsuite entry per module (never invent
*Combinednames). Seereferences/pattern-b-modernization.mdStep 6.
Phase 5b: Migrate in-window OPA to bare-Component iframe
Run only when Phase 0.2 reported needsIframeMigration: true.
Read references/pattern-u-iframe-migration.md for full instructions. Summary:
- Create
opaIframe.qunit.html+opaIframeBoot.js— bare-Component iframe. HTML loadssap/ushellsandbox.js for stubs, nosap-ushell-config. Boot module runsmockserver.init()→new ComponentContainer(...).placeAt("content"). CSP-clean:data-sap-ui-oninit="module:...", no inline script. - Rewrite
arrangements/Common.js—iStartMyApp→iStartMyAppInAFrame(...). Drop mockserver import, drop_clearSharedData. - Rewrite journeys —
iStartMyUIComponent({...})→iStartMyApp()(forward hash/autoWait). - Strip parent-frame mockserver init from OpaSetup.js — now boots in iframe.
- Cross-window control instantiation — UI5 controls in
waitForcallbacks must resolve viaOpa5.getWindow().sap.ui.require(...). OPA-safe deps kept in parent:sap/ui/test/*,sap/ui/core/routing/History. Gate:node <skill-dir>/scripts/detect-cross-window-imports.js <project-root>. - Cross-window jQuery/DOM — replace bare
$(...),document.*,window.*withOpa5.getJQuery()(...),Opa5.getWindow().*. - Routing helpers — plain Component-router hash, no
#app-tile&/prefix. - Mockserver
sap-messageenvelopes — POST handlers needsap-messageheader for message collectors. - ErrorHandler null-guard — guard
getElementsByTagName("message")[0].firstChild.dataagainst null. - Do NOT register
<NAMESPACE>/test/integration/opaIframeinloader.paths.
Items 5–9 are project-specific; apply mechanically per detection grep in reference file.
Phase 6: Delete Old Files
Unit test files
- Delete
unitTests.qunit.html,legacyTests.qunit.html(legacy bootstraps) - Verify all redundant aggregators from Phase 4.0 are deleted
OPA files — Pattern A
- Delete
opaTests.qunit.html,AllJourneys.js,AllJourneys.json
OPA files — Pattern U (only if needsIframeMigration)
- Delete
flpSandbox.qunit.htmlif it exists from prior attempt - Confirm no journey still calls
iStartMyUIComponent
OPA files — Pattern B
- Delete all
*.qunit.htmlunderwebapp/test/opa/(count must matchsummary.totalActive)
Do NOT delete
testsuite.qunit.html(updated),Test.qunit.html(created)
Phase 7: Verify
- Count check: OPA entries in
testsuite.qunit.jsmatch parse script's OPA total. - Dangling-entry check:
node <skill-dir>/scripts/check-dangling-entries.js webapp/test— exit 0 = OK. - Linter:
npx @ui5/linter— noprefer-test-starterwarnings remain. - Structural review:
Test.qunit.html→runTest.js;testsuite.qunit.html→createSuite.js;testsuite.qunit.js→ unit delegate + OPA journeys; all unit files usesap.ui.define;OpaSetup.jsexists; every journey imports it; no stale HTML remains. - Pattern U verification (if
needsIframeMigration):opaIframe.qunit.html+opaIframeBoot.jsexist; no inline<script>body; nowindow["sap-ushell-config"]grep -rn "iStartMyUIComponent\b" webapp/test→ zero hitsgrep -rn "#app-tile&/" webapp/test→ zero hitsmockserver.init()only inopaIframeBoot.js- No
loader.pathsalias for opaIframe/flpSandbox detect-cross-window-imports.jsexits 0- ErrorHandler XML-parse guarded or flagged
Worked Examples
Example A — Pattern A (Single HTML + AllJourneys)
Namespace: com.mycompany.myapp, 4 OPA journeys + 2 unit tests.
After: testsuite.qunit.js → 5 entries (1 unit + 4 OPA). AllJourneys.js → OpaSetup.js + individual entries. Deleted: AllJourneys.json, opaTests.qunit.html, unitTests.qunit.html.
Example B — Pattern B (Many Individual HTML Files)
Namespace: com.mycompany.myapp, 45 OPA journeys + 3 unit tests.
After: testsuite.qunit.js → 46 entries (1 unit + 45 OPA). OpaSetup.js = union of utility imports. 3 journeys with autoWait: false override. Deleted: 46 HTML files + unitTests.qunit.html.
Important Notes
runTest.jsvscreateSuite.js:createSuite.js= testsuite overview.runTest.js=Test.qunit.htmlfor individual tests..qunit.jssuffix rule: Only testsuite entry keys need.qunit.js. Dependencies loaded viasap.ui.define(page objects,OpaSetup.js) keep plain.js..qunitin dependency paths:"./FilterBar.qunit"resolves toFilterBar.qunit.js. Without suffix → file not found.test-resources/prefix: Deps underwebapp/test/usetest-resources/<NAMESPACE>/...not<NAMESPACE>/test/....- Convert existing
<NAMESPACE>/test/deps: After Phase 5, scan ALL test.jsfiles and convert totest-resources/<NAMESPACE>/. - Relative vs
test-resources/: Same-directory →./. Cross-directory →test-resources/<NAMESPACE>/.... - OpaSetup.js must NOT import
sap/ui/test/opaQunit— belongs in each journey file. - Side-effect imports go at END of dependency array — prepending shifts parameter positions.
Completion Checklist
| # | Check |
|---|---|
| 1 | Test.qunit.html exists with runTest.js |
| 2 | testsuite.qunit.html uses createSuite.js |
| 3 | testsuite.qunit.js has "unit/unitTests" + all OPA journeys |
| 4 | No redundant aggregators remain |
| 5 | No stale test HTML bootstraps remain |
| 6 | unitTests.qunit.js references only real test files |
| 7 | OPA entry count matches parse script total |
| 8 | Dangling-entry check passes (exit 0) |
| 9 | Bootstrap overrides reported in MODERNIZATION_ISSUES.md (if any) |
| 10 | Launcher + FLP classified; mixed halted |
| 11 | If needsIframeMigration: iframe files exist, no inline script, no iStartMyUIComponent |
| 12 | If needsIframeMigration: no FLP hash prefix, no loader alias |
| 13 | If needsIframeMigration: ErrorHandler guarded or flagged |
| 14 | If needsIframeMigration: detect-cross-window-imports.js exits 0 |
Related Skills
- fix-csp-compliance — old HTML inline scripts violate CSP; Test Starter removes them.
- fix-linter-blind-spots — catches runtime-breaking patterns the linter misses.
- fix-js-globals (cases 1b/1c) — handles
sap.*globals the linter reports.