Skip to content

Writing test cases

There are two ways to write a verikun test. They produce the same reports and the same exit codes; they differ in who writes the steps.

vk batch vk ai
You write exact commands plain English
Runs deterministically, always deterministically after the first compile
Cost $0 $0 on the happy path; you pay to compile and to repair
Control flow none — a flat list conditions, loops, branches
Recovers from a changed UI no yes, by asking the model to repair the step
Needs a model key no yes, unless you use a CLI backend

A useful rule: batch for a flow you control and want pinned; ai for a flow that drifts or has optional steps.

vk batch reads newline-separated commands — from --file <path>, or piped on stdin — and runs each exactly as if you had typed it as its own vk command: the same selector auto-wait, the same test-run recording (every line is its own step), and the same stdout/stderr split and exit codes.

Terminal window
vk batch --file login.flow # from a file
vk batch <<'EOF' # …or piped on stdin
launch com.example.app
text @email_input "user@example.com"
text @password_input "hunter2" --enter
assert text:"Welcome back" --wait 8s
EOF

What it guarantees:

  • Each result streams to stdout as the command finishes — the same bytes you would get running the line on its own.
  • It stops at the first command that exits non-zero, noting where it halted (on stderr) and exiting with that command’s code.
  • Blank lines and # comments are skipped, so a flow file can be annotated.
  • Globals on the batch call carry into every line unless a line overrides them — --device, --platform / --ios / --android, and --json. So vk batch --ios --file f runs the whole flow against the simulator.
  • --quiet silences the per-line progress notes on stderr; stdout data is untouched.

Ending a batch with run archive turns the flow into a JUnit + HTML report in one shot:

login.flow
# Fresh start — launch force-stops first, so this is a real reset
launch com.example.app --clear
# The field lookups auto-wait up to 5s; no explicit `wait` needed
text @email_input "user@example.com"
text @password_input "hunter2" --enter
# Verification. These are what make it a test.
assert text:"Welcome back" --wait 8s
assert @error_banner --gone
# Turn the recording into report.html + report.xml, and gate on it
run archive login-smoke
Terminal window
vk batch --file login.flow
echo $? # 0 = green, 1 = a step failed, 2/3 = usage or environment

The same test as prose:

login.md
Launch com.example.app fresh.
If a "Allow notifications" dialog appears, dismiss it.
Type user@example.com into the email field and hunter2 into the password field, then submit.
Assert that "Welcome back" is visible and no error banner is shown.
Terminal window
vk ai login.md

The prose is compiled into a deterministic plan once, cached, and replayed with no model calls; the model wakes only to repair a step whose selector stopped resolving. Two things this buys you that batch cannot express: optional steps (If a permission dialog appears, allow it compiles to an if-present guard) and bounded loops (Scroll until the row appears). See Natural-language tests for the full model, and AI plans & models for the plan grammar.

This matters more than the format you choose. In order of preference: @id (the only selector that survives localisation and copy changes), then text:, and desc: only on Android. The reasoning and the per-platform matrix: Selectors.

A test should not depend on what the previous test left behind. Start it from a known state:

launch com.example.app --clear # wipes login/session/prefs, then starts fresh

If you are running a suite, vk suite --app <id> does this between tests for you.

An assertion failure is terminal — vk ai never heals one. That makes assert the place to state what you actually mean:

Terminal window
assert text:"Welcome back" --wait 8s # it appeared within 8s
assert @loading_spinner --gone --wait 15s # it went away within 15s
assert @submit --enabled # it is actually pressable, not just present
assert @email_input --text "user@example.com" # the field holds this value

--enabled deserves special mention: a Submit button the app disables until a form is valid is present long before it is usable. Asserting or tapping presence taps a dead control.