Writing Your First TestCase
Compose TestSteps into a business-flow TestCase — the naming and re-export conventions a scaffolded App follows, and why they look the way they do.
← Back to overview · 🇩🇪 Deutsch · ← FAQ
Prerequisites: you have added your App (see Add a New App), modeled your Controls (see Add Controls), and composed your TestSteps (see Build TestSteps). This page composes those TestSteps into a TestCase.
TestCase structure in one sentence
A TestCase in pm/3_Cases/TC_*.spec.ts composes TestSteps exported from pm/2_Apps/<AppName>/2_Steps/*.ts — nothing more. TestSteps themselves come from one of three sources: a Core.defineTestStep/Core.defineExecutionStep factory, a hand-written function against a Control, or a Custom Step generated by the Authoring Agent. See Concepts for the full three-layer picture (Controls → TestSteps → TestCases) this page assumes you already know.
Step-naming convention
The canonical shape for every TestStep exported from a scaffolded App is:
TS_<Type>_<Action>
| Example | Meaning |
|---|---|
TS_Execution_Start | App-lifecycle step — see Core.defineExecutionStep |
TS_Main_Button_Click | Control interaction on the Main view |
TS_Dialog_Textfield_Fill | Control interaction inside a Dialog |
TS_Custom_MyCheck | Hand-written or agent-generated Custom Step |
Why the App name is not part of the step name
- The barrel namespace already carries it. A step is always called through its App’s barrel —
Project.MyApp.TS_Execution_Start()— so the App identity is already present at the call site. Repeating it inside the function name would only duplicate information the namespace already provides. - Registration is a separate concept from naming. The first argument to
Core.defineTestStep('MyApp', ...)/Core.defineExecutionStep('MyApp', 'Start', ...)is the App’s key inGlobalConfig.apps— it tells the framework which App entry to resolvepage/url/executable/appiumUrlfrom. It has nothing to do with the exported function’s name; the two are independent, and the registration key stays exactly as written even though it no longer shows up in the name. - Agent-authoring-friendly. An authoring agent generating or fixing a step reads region/element/action straight from the function name; which App it belongs to is already unambiguous from the barrel namespace the step is exported through.
- Tester-scanning-friendly. A test-management tool’s step picker already groups steps by App via the barrel namespace — a repeated App-name segment in every function name would only add noise to the list.
💡 Same shape as the older fixture convention. Apps scaffolded from
_SkeletonfollowTS_<Type>_<Action>throughout. This is the same convention already described asTS_<Region>_<ElementType>_<Verb>in Concepts —TS_Main_Button_Clickbreaks down as regionMain, elementButton, verbClick.<Type>above corresponds to<Region>_<ElementType>,<Action>to<Verb>; both names describe the same convention.
💡 Coming from v0.13.0? Step-export names used to include the App name (
TS_MyApp_Execution_Start). From v0.14.0 onwards the App name lives only in the barrel namespace — rename an existing export by dropping its App-name segment (TS_MyApp_Execution_Start→TS_Execution_Start); the call site keeps working once it’s renamed, sinceProject.MyApp.already carried the App identity.
🛡️ Enforced automatically. The framework validates every
TS_*export against this naming convention at commit-time via a pre-commit hook and in CI. See Skeleton Conventions for the full rule-set (naming, signatures, XPath style, i18n).
Re-export convention
An App’s 2_Apps/<AppName>/References.ts barrel re-exports its own folders with two different shapes — steps flat, controls namespaced:
// 2_Apps/<AppName>/References.ts
// Controls — namespaced
export * as ControlsButton from './1_Controls/Control_Button';
export * as ControlsTextfield from './1_Controls/Control_Textfield';
// Steps — flat
export * from './2_Steps/TS_Execution';
export * from './2_Steps/TS_Main';
export * from './2_Steps/TS_Dialog';
Why steps are flat
A step’s App identity is already established by the barrel namespace it’s exported through — Project.MyApp.TS_Execution_Start() — so a further per-step namespace inside that same barrel would add nothing the call site doesn’t already know. Fixture-style Apps in this framework’s own test suite have re-exported their steps flat from the start; the scaffolded _Skeleton follows the same pattern.
Why controls stay namespaced
Controls use generic, repeated method names (click, checkVisible, fill) that only make sense together with their Control’s identity — a flat click(...) export would collide the moment two Controls both expose one. The namespace (ControlsButton.click(...) vs. ControlsLink.click(...)) is what disambiguates them.
Your first TestCase
// 3_Cases/TC_MyFirstFlow.spec.ts
import * as Project from '@GlobalRef';
Project.Core.test('TC_MyFirstFlow', async () => {
Project.Core.Step.setCurrentTestCaseName('TC_MyFirstFlow');
await Project.Core.Step.numberedStepBlock('Run the flow', async () => {
await Project.MyApp.TS_Execution_Start();
await Project.MyApp.TS_Main_Button_Click('Save');
await Project.MyApp.TS_Execution_Close();
});
});
TS_Execution_Start/_Close take no page or url argument at all — Core.defineExecutionStep resolves both from the matching entry in GlobalConfig.apps internally. See API Reference — Section 16 for the full env-shape this factory injects.
A step whose name starts with Get/Read/List returns the value it determined — assign it to a variable and reuse it in a later step or assertion. See Build TestSteps — Return a value from a Get/Read step for the pattern.
Step blocks: grouping, time limits, and custom failure messages
Project.Core.Step.numberedStepBlock(title, body, options?) groups several TS_* calls into a named block (TSB_x) — for report structure and readability. Every block’s duration is measured automatically and recorded as duration_ms; you don’t have to do anything for that.
The optional third argument options unlocks two extra capabilities. Both are opt-in — without options the block behaves exactly as before.
Time limits (durationLimits) — an escalation ladder
A list of thresholds, each with its own level and message:
await Project.Core.Step.numberedStepBlock('Check redirect to AGOV', async () => {
await Project.MyApp.TS_Execution_Browser_Start();
await Project.MyApp.TS_Main_Label_ByClassName_IsEqual('start', '', 'labelQR', 'Scan the QR code');
}, {
durationLimits: [
{ ms: 6000, level: 'warn', message: 'Redirect is slow' },
{ ms: 10000, level: 'fail', message: 'Redirect too slow (SLA breached)' },
],
});
- Evaluated only on success of the block (a failed block’s duration is meaningless).
- Exactly the most severe breached rung fires (
fail>warn>info; ties broken by the largerms). The order in the list does not matter. info(the default whenlevelis omitted) andwarnonly log — the test stays green.failfails the block (assertion-shaped, so it integrates with the run mode: soft infailsafe, hard infailfast).messageis optional; without it a generic “took X ms (over the Y ms limit)” line is used.- When a rung is breached the report additionally carries
limit_ms,limitLevel, andlimitExceeded(otherwise these fields do not appear at all).
Custom failure message (failureMessage)
A domain-readable text prepended to a functional failure of the block — without discarding the underlying cause:
await Project.Core.Step.numberedStepBlock('Check redirect to AGOV', async () => {
await Project.MyApp.TS_Execution_Browser_Start();
await Project.MyApp.TS_Main_Label_ByClassName_IsEqual('start', '', 'labelQR', 'Scan the QR code');
}, {
failureMessage: 'Redirect to AGOV failed',
});
If a step in the block fails, the message reads e.g. Redirect to AGOV failed: <original error>. The original error (including its stack and assertion shape) is preserved — this replaces the anti-pattern catch { throw new Error('...') }, which would throw the cause away. A fail from durationLimits does not additionally get failureMessage prepended (the rung carries its own message).
Both options are available from framework 0.48.0.
Try it against the shipped demo
Rather than writing your own Controls and TestSteps before your first run, you can point a TestCase at the framework’s shipped Demo Web App instead — a small ERP-style app with a matching, ready-to-use preset:
npm install --save-dev @meintest/cc-testframework-demo-web(from insidepm/)- Scaffold the
_ExampleWebApppreset from the templates alongside your project (Controls, TestSteps, and their.i18n.jsoncatalogs come pre-instantiated — nothing to write) - Run the shipped
TC_ExampleWebApp_HappyPath.spec.ts— a complete login → CRUD → logout flow, composed exactly the way Your first TestCase above describes
See Demo Web App for the full setup (including the GlobalConfig.apps entry) and a feature tour.
For file operations, registry access, system-time checks, or arbitrary shell commands, the framework ships two ready-to-use apps — see OS App.
Where to go next
- Concepts — the full three-layer architecture (Core / 2_Apps / 3_Cases) this page assumes
- Skeleton Conventions — the 6 rules the framework enforces on every scaffolded App (naming, signature, XPath, i18n), and how to opt in to full enforcement on your own Apps via
strictConventions - API Reference — Section 16 — the
Core.defineExecutionStepsignature and its family-awareenvshape - Custom Steps — the
@customconvention for a step with no existing Control × Action combination yet - Step-Description Localization — showing a step’s description in a tester’s own language via a sibling
.i18n.jsoncatalog - Demo Web App — a shipped example app and TestCase to run against before wiring your own
Next step: Run and Debug — run this TestCase and iterate on failures.
📧 Questions? Contact: jens.szelag@itsbusiness.ch
itsbusiness AG · Bern · Switzerland