Docs

WND Test

wnd-test runs regression tests against your mainframe (3270) and IBM i (5250) applications. A test signs on, goes through the screens a person would, and checks what the host shows. A suite of them runs from GitHub Actions, Jenkins, Azure DevOps or GitLab on every change, several sessions at once, and writes JUnit XML for your CI's test view and an HTML report with the screen at each failure. It needs no desktop app and no gateway.

Download the product examples: API scripts, Power Query, OData flows, test CI files, the vendored WND Test action and Power BI connector source. Download gateway and test runner binaries from the customer portal.

Tests can also play a session recording instead of reaching a host. Then they run anywhere, with no mainframe and no license, which is how you try it:

wnd-test init mainframe-tests
cd mainframe-tests
wnd-test
Running 3 tests, 3 at once, against recordings

  ✓  1 tests/inquiry-smoke.wndtest › inquiry-smoke (442 ms)
  ✓  2 tests/customer-lookup.wndtest › customer-lookup [12345678] (448 ms)
  ✓  3 tests/back-to-menu.wndtest › back-to-menu (455 ms)

  3 passed (461 ms)
  Report: ./wnd-test-report/index.html

wnd-test is installed with WND Terminal, next to the app (C:\Program Files\WND Terminal\wnd-test.exe, /Applications/WND Terminal.app/Contents/MacOS/wnd-test, /usr/bin/wnd-test). For CI runners it's also a small download on each release, in WND-Test-Linux-x64.zip, WND-Test-Linux-arm64.zip, WND-Test-Mac-universal.zip and WND-Test-Windows-x64.zip. On Linux it needs OpenSSL 3, which CI images have.

A test

A test is a .wndtest file: a WND Terminal macro (macros.md) with a short header, and assert lines that check the screen.

#! test: customer-lookup
#! description: Looks up a customer and checks the name and status.
#! connection: PROD
#! tags: smoke lookup
#! timeout: 60
wait "CUSTOMER INQUIRY"
move 6 18
type "12345678"
press Enter
wait "STATUS:"
assert row 8 has "ACME TRADING CO"
read status after "STATUS:"
assert status = "ACTIVE" "Customer 12345678 should be active"

On a small screen, swipe the table to read all columns.

Header What it does
#! test: name Makes the macro a test. Lowercase letters, digits and dashes.
#! description: ... What it checks, for people reading the report.
#! connection: PROD The connection it runs in, from wndtest.toml (or one of your organization's policy connections). When the suite has one connection, tests use it without saying.
#! replay: logon.wndrec Play this recording instead of connecting to a host (see below).
#! tags: smoke payments Words to pick tests by (--tag smoke).
#! data: accounts.csv Run the test once for each row of this file, with its columns as variables.
#! timeout: 60 Seconds the test may take (the suite's timeout, else 120, when it doesn't say).

The body is the macro language: wait, move, type, press, read, if, repeat, call and the rest. Two statements are for tests:

  • assert CONDITION checks any condition (screen has "SAVED", row 3 has "INQUIRY", balance = "0.00", not screen has "ABEND"). When it isn't true the test fails at that line: Line 9: expected status = expected_status (status is "ACTIVE", expected_status is "CLOSED"). When the condition compares variables, their values are added.
  • assert CONDITION "message" says it in your words.
  • fail "message" ends the test as failed, for a case you check yourself.

A wait that runs out, a key the host refuses or a line the macro can't do also fail the test, at their line. A test passes when its last line has run.

Files a test names (replay:, data:, macros it calls) are looked for next to the test, then in the suite's folder.

A suite

A suite is a folder: wndtest.toml at the top, and tests (.wndtest files) in it and in the folders under it. wnd-test init makes one.

[connections.PROD]              # the same settings as a connection in your policy
host = "mvs.example.com"
port = 992
secure = true
# model = 4                     # 2 (24x80, the default), 3, 4, 5, or a size such as "62x160"
# code_page = 1141
# lu = "TCPPOOL1"
# host_type = "5250"            # IBM i ("3270" is the default)
# proxy = "socks5://proxy.example.com:1080"

[connections.PROD.logon]        # signs on in each session before the test
macro = "logon.wndmacro"        # a macro in the suite's folder
user = "$TEST_USER"             # {user} in the macro
password = "$TEST_PASSWORD"     # {password} in the macro

[variables]                     # values every test can use
region = "US"

[run]
jobs = 4                        # tests at once, each in its own session
retries = 1                     # run a failed test again, once
timeout = 120                   # seconds a test may take
# total_timeout = 1800          # seconds the whole run may take
report = "wnd-test-report"      # the report folder

A value written $NAME is read from the environment variable NAME when the run starts. Passwords must come that way, and wnd-test refuses a suite with a password written in the file. In CI, keep them in the secret store (GitHub secrets, Jenkins credentials, Azure secret variables, GitLab masked variables) and pass them in as environment variables. The logon macro gets {user}, {password} and any other values you list under [connections.PROD.logon.values].

A logon macro for TSO might be:

wait "Logon ===>"
type {user}
press Enter
wait "ENTER PASSWORD"
type {password}
press Enter
wait "READY"

Running

wnd-test [FOLDER] [options]

On a small screen, swipe the table to read all columns.

Option What it does
--grep TEXT Only tests whose name or file has the text. More than once: any of them.
--tag TAG Only tests with the tag. More than once: any of them.
--replay-only Only tests that play a recording.
--jobs N Tests at once (default: the suite's jobs, else 1). Each has its own session.
--retries N Run a failed test again up to N times. A test that passes on a retry is marked so.
--timeout SECONDS How long a test may take, unless it says.
--total-timeout SECONDS How long the whole run may take. Tests still running then fail, and tests not started are reported as not started.
--junit FILE, --json FILE, --html FILE Also write the JUnit XML, the JSON or the HTML report there.
--report FOLDER, --no-report Where the report folder goes, or none.
--list List the tests and data rows without running them.

The exit code is 0 when every test passed, 1 when a test failed, and 2 when the suite couldn't run. A suite can't run when a test file has a mistake, a connection isn't in wndtest.toml, an environment variable isn't set, there's no license for tests that connect to a host, a host can't be reached, or a logon didn't work. With exit code 2 for a mistake in the files, nothing runs, so a typo never looks like a passing run.

Each test runs in a session of its own: connect, sign on, run the test, close. A suite's tests don't share screens, so they can run in any order and at the same time.

Tests against a recording

#! replay: customer-inquiry.wndrec plays a session recording (from Session recording, see replay-host.md) as the host. Each test gets its own replay host on this computer, in loose mode: the key you press must be the one the person pressed, and the next screens come back as they were recorded. What you type isn't compared.

Use it for tests of the tests, for CI with no route to the mainframe, and to show people how it works. A suite where every test plays a recording runs without a license.

Data-driven tests

#! data: customers.csv runs the test once for each row. The first row names the columns, and each column is a variable in the test:

customer,expected_name,expected_status
12345678,ACME TRADING CO,ACTIVE
87654321,GLOBEX LTD,CLOSED
type customer
press Enter
read name after "NAME:"
assert name = expected_name

Each row is a test case of its own in the reports, named after the first column: customer-lookup [12345678]. Commas, semicolons or tabs between values all work, and values in quotes can have commas in them. A column whose name says it's secret (password, pin, token) is masked in the reports.

Recording a test

Do the task once in WND Terminal with the session recorded, then turn the recording into a test:

  • In the desktop app: Session recordings, open the recording, Save as test…. The test goes next to the recording, set to play it.
  • From the command line: wnd-test record tests/inquiry.wndtest --from customer-inquiry.wndrec. Add --connection PROD to run it against the host instead.

For each screen it writes a wait for a line that only that screen has (its title, or else the first line no other screen has), the fields that were typed into (move, press EraseEOF, type), the key, and an assert on the title of the screen that answers. A value the recording hid (a password, a masked number) becomes a {name} the test types: give it a value under [variables], with $NAME for a password. Then keep the lines that matter and add the checks you care about.

Reports

The console shows a line for each test as it finishes, then each failure with its file and line, the message and the screen at that moment in a box.

The report folder (wnd-test-report/ unless the suite says) has:

  • index.html: one file with everything in it, nothing loaded from outside. Totals, a filter by result and a search, and each test with its steps, each step's time, and the screen at a failure.
  • junit.xml: a testsuite for each test file and a testcase for each test and data row, with the failure's message and line, the step log and the screen. Jenkins (junit), Azure DevOps (PublishTestResults), GitLab (artifacts:reports:junit) and GitHub (dorny/test-reporter) read it.
  • results.json: each test's result, with every step's line and time, the variables at the end, and the failure.
  • A folder for each failed test with screen.txt and screen.html.

Everything in a report is masked the way session recordings are: password fields are blank, card numbers and your organization's MaskPatterns are dotted out, and so are the logon's password and any value whose name says it's secret, wherever they appear.

In CI

examples/test/ has a complete pipeline for each:

  • github-actions.yml: downloads wnd-test, runs the suite, keeps the report and shows the results with dorny/test-reporter.
  • Jenkinsfile: credentials from Jenkins, junit 'wnd-test.xml', and the build marked unstable when a test fails.
  • azure-pipelines.yml: secret variables, the license as a secure file, PublishTestResults.
  • .gitlab-ci.yml: masked variables, the license as a file variable, artifacts:reports:junit.

Download the runner ZIP for your CI platform from the customer portal. Mirror that ZIP in your internal artifact store and configure WND_TEST_DOWNLOAD_URL in the pipeline secrets (Jenkins: the wnd-test-download-url secret-text credential). Pin the mirrored file to the version you have validated. Portal download links expire after ten minutes; use your internal mirror for recurring CI. The product examples archive also includes a vendored action/ directory for GitHub Actions, with a required download-url input.

The runner needs to reach your host on its TN3270 or TN5250 port. A runner inside your network (a self-hosted GitHub runner, a Jenkins agent, an Azure self-hosted agent, a GitLab runner) is the usual way.

The license

Tests that connect to a host need a license that includes WND Test. wnd-test looks for it next to itself (wnd-terminal.license, or any .license file there), in WND_TERMINAL_LICENSE (a path), in the License and LicenseFile policy settings, and in the computer's WND Terminal folder (next to policy.json). Without one it says so and runs nothing (exit code 2). Suites where every test plays a recording, and --replay-only runs, need no license.

wnd-test doesn't write to WND Terminal's audit log. Its report is its record.

How it's built

crates/wnd-test: suite.rs reads the folder and makes the cases (data rows, tags, grep), config.rs reads wndtest.toml, runner.rs opens each session with WND Terminal's engine (wnd_app::Core, as the gateway does, with no window) or a replay host (wnd_core::replay), signs on and runs the test with wnd_core::macros::run_outcome, and report.rs writes the console, JUnit, JSON and HTML. The test header is read by wnd_core::flows, the same parser flows use. wnd-test record and Save as test… share wnd_app::recordings::test_from_recording.

Try it on your own screens

Start a team trial or ask us to plan a proof of value.