ANY//DOCS
DEBack to site

Unit Testing

Integrated unit test framework for TIA blocks. Write, run and evaluate tests against a live PLCSIM Advanced instance without leaving the app.

Requirements:

  • Enterprise license
  • PLCSIM Advanced V3.0+ installed (separate Siemens license)
  • A TIA project with at least one PLC open

Opening the Unit Testing Workspace

  1. Click the Unit Testing icon (beaker) in the Activity Bar on the far left
  2. The Side Bar then shows the two Unit Testing views:
    • Test Suites — the explorer tree of discovered .tia-tests suites, with title-bar actions New Test Suite, Refresh Test Suites, Run All Test Suites / Stop Test Run, and a per-suite inline Run Test Suite
    • Test Results — live progress and per-assertion details (Total / Passed / Failed / Errors)
  3. Opening a suite from the tree opens it as a Test Suite Editor in the editor area in the center. The editor shows the same suite in three switchable views — Visual, JSON and SCL — plus an Open as JSON Text escape hatch; its toolbar holds Save, Open as JSON Text, Validate, and Add Test Case.

Test Explorer

The Test Explorer auto-scans the {WorkingDirectory}/.tia-tests/ folder and shows every .json file as a suite node with its test cases as children.

Status icons (loaded from SQLite on startup):

  • Pass — green checkmark, test passed in the last run
  • Fail — red cross, test failed in the last run
  • Error — orange warning, test errored (exception, timeout, S7 error)
  • Skipped — grey slashed circle, test was filtered out of the last run
  • NotRun — grey empty circle, no persisted result yet

Interactions:

Action Result
Double-click a suite (or select it and press Enter) Opens the suite as a Test Suite Editor in the editor area
Hover a suite → inline Run Test Suite Runs this suite only
Run All Test Suites (view title bar) Runs all visible suites sequentially
Stop Test Run (view title bar, while a run is in progress) Cancels the current run at the next safe point
New Test Suite / Refresh Test Suites (view title bar) Create a new suite, or rescan the .tia-tests/ folder

Planned for the in-app workspace. A search box to filter suites by name, a ✗ Failed-only toggle, a per-case Run Test action, and a Run Affected Only action (re-run only suites whose underlying block changed) are planned. Today, single-suite and all-suites runs are available as above; on the command line, tia-test-runner run --rerun-affected already runs only the suites whose underlying block changed (see CI/CD Integration).

Planned — Block-Change Badge. A small orange dot next to a suite whose underlying PLC block changed since its last run, with a "Block changed since last run" tooltip, is planned for the Test Suites view.

Test-Case Metadata

Each test case in the visual editor of a suite has an expandable Metadata pane (collapsed by default, located below the variable-watch list). Click the section header to expand it and edit the following fields per case:

Field Purpose
Order Numeric run order. Leave empty to keep the natural order in the suite
Tags Comma-separated labels (e.g. safety, regression) used for filters in HTML reports
Priority One of Low / Normal / High / Critical. Drives report sort order
Owner Free-text owner (team name, e-mail, etc.)
Requirements Comma-separated requirement IDs (e.g. REQ-123, REQ-456) for traceability

Schema-validation issues that target a specific test case (such as an out-of-range priority) are shown in a red panel directly inside the affected case's Metadata pane. Issues that target the suite as a whole still appear in the suite-level validation banner. All edits are auto-saved with the rest of the suite. Suites created before this release load with empty metadata defaults — no migration is required.

Live Progress During a Run

While a test is running, the workspace shows:

  • Status Bar: A progress indicator shows that the run is in progress (e.g. "Running '<suite>'…") while the runner works through its phases — creating the PLCSIM instance, compiling, connecting, running each case
  • Test Suites view: Status icons update live per test case (pass/fail appear as each case finishes)
  • Test Results view: The test case list fills up incrementally as results come in
  • Stop Test Run: Cancels the current run at the next safe point (the S7 connection and PLCSIM instance are always cleaned up)

Results Panel

After a run completes, the Results panel shows:

  • Summary: Total / Passed / Failed / Errors counts
  • Test case list: Every case with its status icon and name; a failing case also shows its error message
  • Detail grid (per case): Every assertion with Variable, Operator, Expected, Actual, ✓/✗

Planned — Variable Timeline chart. An in-app chart showing how each watch variable changed cycle by cycle during the run (with a per-variable checkbox legend and a hover crosshair) is planned for the Test Results view. Today the watch data is collected and rendered as a time-series chart in the HTML reports produced by the tia-test-runner command-line tool (see CI/CD Integration).

To collect watch data, add a "watch": ["Var1", "Var2"] array to a test case in the suite JSON and set "cycles" greater than 1. The sampling is capped at 100,000 cycles per test case.

Test results are persisted to %LocalAppData%\AnyAutomation Studio\db\test_results.db — they survive app restarts and reappear in the Test Suites view automatically.

Run History

Planned for the in-app workspace. Run-history browsing, Compare Runs, and Trend are described below as the full feature set. Today these are produced by the tia-test-runner command-line tool (HTML / CSV reports — see CI/CD Integration); the dedicated in-app views are planned. The in-app Test Results view already shows the latest run's per-assertion detail.

The Run History view lists every run that has been persisted to the database, regardless of which suite executed.

  • Columns: Status icon, Started At (local time), Duration, Pass / Fail / Error / Skipped counts, Suite, PLC, Hostname, TIA version
  • Status icon: ✓ green when all cases passed, ✗ red on any failure or error
  • Filter box: Filters the list client-side by hostname, suite name, PLC, or block name (case-insensitive)
  • Refresh: F5 reloads the list (or use the Refresh entry in the context menu)

Context menu actions (right-click a run):

Action Result
Open Results Loads that run into the Test Results view so you can inspect each case
Compare To… Marks this run as the baseline and opens the Select run to compare dialog. Pick a second run and the Compare Runs view renders the diff
Delete Run Permanently removes the run row plus all its test cases and provenance metadata. Block hashes survive so block-change comparisons keep working
Export HTML Renders the run as a self-contained HTML report (<run-id>.html) with the same styling as the in-app Results panel
Open Manifest Opens the SHA-256 integrity manifest for the run (when one was generated by the report pipeline)

The list shows the most recent 100 runs by default, ordered newest-first. Provenance values such as hostname and TIA version come from the run-provenance metadata captured when the run started; the project path itself is never stored in plain text — only an irreversible SHA-256 hash, so you can correlate runs across machines without leaking workspace paths.

Compare Runs

The Compare Runs view is opened on demand by Compare To… in Run History — you pick a baseline run, the launcher dialog asks for a second run, and the Compare Runs view activates with the diff.

  • Header: Baseline / Current run-IDs side by side, Swap button reverses the comparison, Export HTML writes the diff as a self-contained HTML report (the same renderer the tia-test-runner compare command uses)
  • Summary pills: Five colour-coded counters — Regressions (red), Fixed (green), New (blue), Removed (grey), Stable (muted). Stable counts only Still-Passing and Unchanged cases; Still-Failing cases stay flagged as warnings, not as stable
  • Filter bar: Five radio buttons (All / Regressions / Fixed / New / Removed) reduce the case list in place
  • Case rows: Each case shows a coloured bullet (red / green / blue / grey / orange) plus <Suite> / <Case>, the failure message (if any), the change-kind label, and the duration delta versus the baseline (+12 ms / −5 ms)

The launcher dialog lists the most recent 100 runs minus the baseline you started from, sorted newest-first. Double-click a row or click OK to commit the pick; Cancel closes the dialog without changing the comparison.

Trend

The Trend view visualises pass-rate, duration, and per-case status over time using historical runs from the database.

  • Filter bar (row 1): PLC name (required), suite name (optional — narrows the duration chart), case name (optional — narrows the heatmap)
  • Filter bar (row 2): From / To calendar pickers (clear a picker to remove the bound), Last 30 days / Last 90 days quick buttons, Refresh to re-query, Export CSV to write the current trend to a CSV file (the same export the tia-test-runner trend command produces)
  • Pass rate chart: Time-series scatter, X = run start time, Y = pass rate %. Visible once the project query returns at least one point; otherwise shows "No data".
  • Duration chart: Time-series scatter, X = run start time, Y = suite duration in seconds. Requires a suite name; otherwise shows "No data".
  • Case heatmap: Wraps cells left-to-right, one per run, colour-coded by status (green = pass, red = fail, orange = error, grey = skipped) plus a glyph (//!/) for accessibility. Hover any cell to see the run timestamp. Requires both suite and case names; otherwise shows "No data".

If the project trend query fails (database offline, corrupt range), the bottom banner shows the error message. Suite and case query failures show their own red banner above the affected chart so it is clear which section failed; the other charts still render normally.

Block Analysis (before running tests)

When you open a suite, AnyAutomation Studio extracts the block interface from the TIA project so the editor knows the real parameter names and types, and so the inputs and assertions grids can validate against them.

Planned for the in-app workspace. A dedicated analysis surface with Interface, Boundary Values, and Dependencies panes is planned. The data it would summarise:

  • Interface: All block parameters (Input, Output, InOut, Static, Temp) with types and default values
  • Boundary Values: Auto-generated min/max/zero/one values per parameter type
  • Dependencies: Called blocks, referenced DBs, referenced UDTs

Creating a New Test Suite

  1. Click New Test Suite in the Test Suites view title bar (or run it from the Command Palette)
  2. Answer the three prompts that appear in turn: the Suite name, the Block under test, and the PLC name
  3. The new suite is written to <projectDir>/.tia-tests/{name}.json and appears in the Test Suites view
  4. The Test Suite Editor opens with a starting test case wired up with arrange / act / assertions / watch so you can see how the pieces fit together. The block interface is extracted from the TIA project, so the inputs and assertions grids validate against the real parameter names. Always replace placeholder values with real names before pointing the runner at hardware.
  5. Use the Visual mode (form-based master-detail) to add / remove / duplicate test cases through fields and grids — Name, Description, Cycles, Tags, Priority + Arrange-Inputs grid + Assertions grid + Watch list. Switch to Open as JSON Text anytime to edit the raw JSON. The SCL view shows the generated SCL test code and is editable too (see Managing Suites and Test Cases)
  6. Click Save (Ctrl+S) to persist your changes

Renaming a suite from the JSON name field

When you edit the name field at the top of the suite JSON and press Save, the file is renamed on disk to match — {newName}.json — and the editor title updates in the same step. If a suite with the new name already exists in the same folder, Save aborts with an error and leaves both files untouched so you can pick a different name.

The blockName field is independent and continues to identify the TIA block under test; it is not affected by a name rename.

Test Suite JSON Schema

Every .tia-tests/*.json file follows the same shape. Top-level fields:

Field Type Required Purpose
name string yes Suite identifier (must match the file name without .json)
blockName string yes Exact TIA block name (case-sensitive)
plcName string yes PLC/CPU name in the TIA project (e.g. PLC_1)
config object yes Connection + transport settings (see below)
testCases array yes One entry per test case (see below)

config (all fields optional, defaults shown):

Field Default Purpose
cycles 1 Default PLC cycles per test case (overridable per case via act.cycles)
cycleWaitMs 100 Wait between sampled reads when cycles > 1
timeoutMs 5000 Hard timeout per test case
instanceDbName "" Instance-DB to read/write; empty → <blockName>_DB (Siemens convention)
preferFc false Tolerate FC blocks (no Instance-DB); set when the target is a function instead of a function block
transport "s7CommPlus" "s7CommPlus" for real PLC / TCP-PLCSIM, "plcSimApi" for direct PLCSIM Tag-API
s7IpAddress 192.168.0.1 S7 connect target — used only when preparationMode = "external" (real PLC). For PLCSIM-backed modes (userPreloaded, tiaTcpDownload) the runner connects to plcSimIp instead, regardless of transport
s7Port 102 S7 ISO-on-TCP port. Default 102 matches every Siemens default — change only when the PLC was reconfigured to a non-standard TCP port
s7User, s7PasswordKeyId "" / null Credentials reference (password lives in Windows Credential Manager). Required only when the PLC enforces user authentication — most projects leave this empty
plcSimInstanceName "TiaUnitTest" PLCSIM Advanced instance name. Used by both transports whenever preparationMode != "external"
plcSimIp "192.168.0.100" PLCSIM Virtual-Adapter IP. This is the actual S7 connect target for s7CommPlus + userPreloaded/tiaTcpDownload — set it to the IP you assigned to the Siemens PLCSIM Virtual Ethernet Adapter, not to the PLC project IP
preparationMode "userPreloaded" userPreloaded (default; assumes the PLCSIM instance is already running with your project loaded), tiaTcpDownload (compiles + downloads from the open TIA project — see prerequisites below), or external (real PLC; uses s7IpAddress)
masterSecretPasswordKeyId null Project master-secret password reference. Required for tiaTcpDownload only when the TIA project uses confidential PLC configuration protection. Password lives in Windows Credential Manager
autoConnectS7 true Auto-connect S7 before run starts. Set to false only in advanced setups where another client (WinCC, custom HMI) already holds the session and the runner should read/write through it
keepInstanceAfterRun false Keep PLCSIM instance alive after the run. Has effect only in tiaTcpDownload mode — userPreloaded and external never manage the instance lifecycle

Each entry in testCases:

Field Type Required Purpose
name string yes PascalCase scenario name (e.g. Start_FromIdle_Runs)
description string no One-sentence what-and-why
arrange.inputs object yes { "MemberName": value, … } — written to the DB before Act
act.cycles int no (defaults to config.cycles) PLC cycles between Arrange and Assert; must be > 1 for watch[] to fire. Mutually exclusive with act.steps (the schema's oneOf rejects both on the same act)
act.timeoutMs int no (defaults to config.timeoutMs) Per-case timeout. Covers the entire steps sequence end-to-end when steps is set
act.steps array no Optional ordered sequence of write / wait / assert steps that runs between Arrange and the top-level assertions. See Multi-phase Steps below. Mutually exclusive with act.cycles
assertions array yes [ { "variable": "X", "operator": "equal", "expected": v, "tolerance": t? }, … ]
watch string[] no Variables sampled once per cycle during Act → time-series chart in the HTML reports
tags string[] no Free-form labels
priority string no "low" / "normal" (default) / "high"
requirements string[] no Traceability links
order int no Stable sort key in the UI

Supported operator values: equal, notEqual, greaterThan, lessThan, inRange (expects expected: [min, max]), isTrue, isFalse. tolerance is meaningful only on Real/LReal with equal/notEqual (default 0.0001). expected is required by the schema for every assertion (set it to any value — false, true, 0 — for isTrue/isFalse, since the runner ignores it).

Safety — example values are placeholders. When the editor cannot infer real Input/Output names from the block interface, the seeded example uses safe defaults (Enable: false, Running: false) so a one-click Run cannot accidentally activate motors, valves, or other outputs on real hardware. Always review every arrange.inputs value and every assertion before pointing the runner at a real PLC.

Important — watch[] is the test case's diagnostic array, NOT the TIA Watch Table. Variables listed in watch are sampled once per PLC cycle during the Act phase (provided act.cycles > 1) and rendered as a time-series chart in the HTML reports. This is independent of TIA Portal's external Watch Table or any OPC UA subscription.

Complete example

A full minimal suite with one test case that exercises every block and uses watch[]:

{
  "name": "FB_Motor_Tests",
  "blockName": "FB_Motor",
  "plcName": "PLC_1",
  "config": {
    "cycles": 1,
    "timeoutMs": 5000,
    "instanceDbName": "FB_Motor_DB",
    "transport": "s7CommPlus",
    "s7IpAddress": "192.168.0.1",
    "s7Port": 102,
    "autoConnectS7": true,
    "preparationMode": "userPreloaded"
  },
  "testCases": [
    {
      "name": "Start_FromIdle_RunsWithinThreeCycles",
      "description": "Asserting Run goes high after StartCmd is set; watch shows trajectory.",
      "arrange": {
        "inputs": {
          "StartCmd": true,
          "Reset":    false,
          "Speed":    50.0
        }
      },
      "act": { "cycles": 5 },
      "assertions": [
        { "variable": "Run",       "operator": "equal",     "expected": true },
        { "variable": "Speed_Act", "operator": "inRange",   "expected": [49.5, 50.5] },
        { "variable": "Error",     "operator": "isFalse", "expected": false }
      ],
      "watch": ["Run", "Speed_Act", "StartCmd"],
      "tags": ["happy-path", "smoke"],
      "priority": "normal"
    }
  ]
}

After the run, the Test Results view shows pass/fail per assertion, and the HTML report renders a time-series chart for the three watch variables across the 5 cycles. To add more variables to watch on an existing case, append to the watch array and increase act.cycles to the smallest number that gives useful coverage (3-10 is typical).

Multi-phase Steps (act.steps)

When a single Arrange-then-Assert is not enough — for example reset → wait → start → wait → check — use act.steps instead of act.cycles. Three step types are available:

type Required fields Effect
"write" inputs (object, same shape as arrange.inputs) Writes the listed members additively on top of whatever is already in the DB. Members not listed keep their value.
"wait" ms (int, 0..3 600 000) Async delay. The PLC keeps cycling. When watch[] is non-empty and config.cycleWaitMs > 0, watched variables are sampled once every config.cycleWaitMs ms throughout the wait window.
"assert" assertions (array, same shape as the top-level assertions) Per-step checkpoint. A failure aborts the case immediately and reports Step <N> (assert): <reason> (1-based).

Order of operations:

  1. arrange.inputs is written once at case start (not replayed between steps).
  2. Each entry in steps runs in declaration order.
  3. After the last step, the top-level assertions[] runs as a final gate. Per-step asserts and top-level asserts are independent — both must pass. The schema requires minItems: 1 on the top-level assertions, so author at least one trivial top-level assertion even when relying mainly on per-step asserts.
  4. watch[] continues to sample throughout every wait step (under the conditions above); the time-series chart annotates step boundaries.

Caps (enforced by the runner):

Limit Value
Steps per case 1024
Inputs per write step 256
Assertions per assert step 256
wait.ms 0..3 600 000 (1 hour)

Visual editor: the case detail view shows a Steps (optional) section with + Write, + Wait, + Assert buttons to append, per-step ↑ / ↓ buttons to reorder, and to remove. The editor automatically omits cycles from the serialized JSON whenever the Steps section is non-empty (the schema rejects the cycles + steps combination, so the editor avoids ever producing it).

Backwards-compatibility: a case without steps runs the existing single-phase cycles-based path with no behavioural change. Older suites need no migration.

Complete example — timed motor start

StartCmd is set after a 500 ms reset window, then we wait 2 s for the FB to reach Run, plus a final speed check.

{
  "name": "MotorStart_ReachesRun",
  "description": "Reset, wait, start, wait, check Run.",
  "arrange": { "inputs": { "Enable": true } },
  "act": {
    "timeoutMs": 10000,
    "steps": [
      { "type": "write", "inputs": { "Reset": true } },
      { "type": "wait",  "ms": 500 },
      { "type": "write", "inputs": { "Reset": false, "StartCmd": true } },
      { "type": "wait",  "ms": 2000 },
      { "type": "assert", "assertions": [
          { "variable": "Run", "operator": "isTrue", "expected": true }
        ]
      }
    ]
  },
  "assertions": [
    { "variable": "Speed_Act", "operator": "greaterThan", "expected": 1000 }
  ],
  "watch": ["Run", "Speed_Act"]
}

If the per-step assert fails (for example Run did not go true within 2 s), the runner emits Step 5 (assert): variable Run is not true and aborts the case. If the per-step asserts all pass but the top-level Speed_Act > 1000 fails, the case fails with the top-level assertion message.

When in doubt, write a plain Arrange + cycles ≥ N + Assert case first. Convert it into a steps sequence only when the plain form cannot express the timing the test actually depends on.

Opening an Existing Suite

  • From the Test Suites view: Double-click the suite in the tree (it opens in the Test Suite Editor)
  • From Quick Open: Press Ctrl+P and type the suite file name under .tia-tests/

Where are test suites stored?

Test suites live in a hidden folder called .tia-tests directly next to your TIA project file (*.ap21, *.ap20, …). The AI assistant writes to the same folder. If you haven't opened a TIA project yet, the Test Suites view shows "No TIA Project connected" — connect a project first.

The AI can read, list, and edit these test-suite files directly through the chat interface.

Running Tests

  1. Make sure PLCSIM Advanced V3.0+ is installed
  2. Open the suite from the Test Suites view
  3. Run it: use the per-suite inline Run Test Suite action in the Test Suites view, Run All Test Suites from the view title bar, or a right-click context-menu action
  4. Watch the live progress in the Status Bar and the Test Results view
  5. When the run is complete, status icons in the Test Suites view update and results are persisted to SQLite

The runner automatically:

  • Creates a PLCSIM Advanced instance (name configurable in the suite JSON)
  • Powers it on and compiles the current PLC project
  • Connects an S7 client to the instance
  • Writes the test inputs, waits the configured cycle count, reads the outputs, evaluates the assertions
  • Cleans up the S7 connection and unregisters the instance

Choosing a Transport

The transport decides how test reads and writes travel between AnyAutomation Studio and the PLC. You set it in the suite JSON via config.transport (see the schema above):

  • PLCSim Advanced (plcSimApi) — Direct access via the Siemens PLCSim API, faster and does not need an active S7 session. Best for simulator-only testing.
  • S7 Native (s7CommPlus) — Uses the S7 Communication driver over TCP/IP. Pick this when you want to run the tests against real hardware (in combination with the Real PLC preparation mode) or against PLCSim through the virtual Ethernet adapter with user authentication. Same protocol used by the PLC Online view.

Each suite is targeted independently. For S7 Native, the suite owns its IP, TCP port, username, and password — no cross-feature lookup from the PLC Online view. Rack and slot are not configurable: S7-1200/1500 connect via the S7CommPlus protocol with a fixed routing handshake, and TLS is unconditional on every connection — both are taken care of by the transport.

Planned for the in-app workspace. A Connection Settings dialog that edits these config values from a form is planned. Today you edit them directly in the suite JSON (Open as JSON Text in the Test Suite Editor).

Simulation Workspace

Planned for the in-app workspace. A dedicated Simulation view for managing PLCSim Advanced instances from inside AnyAutomation Studio is planned. The capabilities it would expose are described below.

A Simulation view for managing PLCSim Advanced instances would show:

  • API version, online-access mode (PLCSim / TCP/IP single / TCP/IP multi), strict motion timing toggle, and Runtime Manager Port.
  • A Virtual Adapter row showing the permanent status of the Siemens PLCSIM virtual Ethernet adapter (Ready / APIPA / Disabled / Not Installed / No IPv4), its IPv4 address, and a refresh button. If the adapter is stuck in the 169.254.x.y APIPA range, downloads will be unreliable — assign a static IPv4 address in Windows network settings.
  • Every registered PLCSim instance with inline action buttons: Power On, Run, Stop, Memory Reset, Power Off, Settings, Network, Delete. Each row shows the configured IP address; if the instance has more than one interface with an IP, they are listed as X1: 192.168.0.1, X2: 10.0.0.5.
  • A TIA PLC combobox per instance card: pick the TIA Portal PLC that backs the instance. Use this when the PLCSim instance was loaded with a snapshot compiled from a different TIA PLC than the one it is registered under (for example: an instance named PLC_2 actually holding the program from PLC_1). Without an explicit choice, the app matches by name and falls back to the only PLC in the project when there is one — which is enough for most setups but produces an empty tag tree when the names disagree. The choice is remembered per instance.
  • A New Instance button that prompts for a name and CPU type.
  • A Tag Browser pane that connects to any listed instance and shows all tags the running program exposes, filterable by name search, area (Input / Output / Marker / Data Block), and data type. Auto-refresh can be toggled for live value observation. Each writable tag has a pencil button that opens a type-aware write dialog (toggle for Bool, numeric input with type-specific range for integers and floats, one-character field for Char/WChar). String tags are read-only. Struct tags and array tags are listed with their inner members and elements as expandable rows so you can search, watch and edit individual fields like OUT[5] or DI10.Channel directly; Expand all and Collapse all toolbar buttons toggle the whole tree at once. Structured input/output tags whose layout comes from a project type definition (for example a PNPN block at %I0.0 declared as Array[0..63] of Byte) drill down into named member rows when the app is connected to the TIA Portal project that defines them — letting you write IN.PNPN[12] from the same dialog that handles the rest of the tree.
  • A Saved Instances section at the bottom listing every PLCSim Advanced instance persisted on the virtual SIMATIC memory card. The heading shows a counter with the total number of saved instances; the list shows up to three rows at a time and scrolls if there are more so the simulation area above stays visible. Click Load to re-register and resume one — PLCSim picks up the stored CPU type, I/O image and program automatically. Click Delete (with confirmation) to remove the persisted folder.

The view choice would be remembered between sessions, and switching keeps any open suite editors intact.

PLCSIM Preparation Mode

The preparation mode controls how the runner brings your project online before the test run. It is set in the suite JSON via config.preparationMode:

  • I load it myself (userPreloaded, recommended) — The runner expects a PLCSIM instance that you have already started and loaded manually via TIA Portal. It skips compile and download and connects directly to run the tests. Fastest option if you're iterating on the same project.
  • Automatic TCP download (tiaTcpDownload) — The runner compiles the project, starts a fresh PLCSIM instance and downloads via TCP over the PLCSIM Virtual Adapter. No manual TIA Portal interaction needed — just run.
  • Real PLC (no PLCSim) (external) — Skips the PLCSim lifecycle entirely. The runner connects S7 Native directly to the live S7 hardware at the IP and TCP port you configured and runs the tests there. Use only when the plant is in a safe test state. This mode requires the S7 Native transport; PLCSim API is incompatible.

Before using Automatic TCP download: Four prerequisites must be met or the run will fail with a generic transport error: (1) the PLCSIM Virtual Adapter must be installed and have a static IPv4 address that matches the suite's plcSimIp; (2) the TIA Portal project must be open in TIA Portal — the runner triggers a compile against the open project before the download; (3) if the project enables Protect confidential PLC configuration data, you must supply the master-secret password (it lives in the Vault); (4) any existing PLCSIM instance with the same name is destroyed and recreated for every run, so other applications attached to that instance lose their session.

Running Tests Against a Real PLC

Setting preparationMode to Real PLC (external) enables tests against live S7-1200 / S7-1500 hardware without any PLCSim involvement:

  1. Make sure the plant is in a safe test state — actuators isolated or interlocked, no production load on the PLC, operators informed.
  2. In the suite config, choose S7 Native (s7CommPlus) as the transport, set the PLC's real IP address and (if the PLC was reconfigured to a non-standard TCP port) the port; supply user/password if the PLC enforces authentication.
  3. Set the preparation mode to Real PLC (no PLCSim) (external).
  4. Run the suite from the Test Suites view.
  5. Before the runner contacts the PLC, an acknowledgement dialog appears: "Run tests against live PLC?" Read the warning, tick "I understand — the plant is in a safe test state.", then click Run tests. The button stays disabled until the checkbox is ticked. Cancel aborts the run with no network traffic.

Safety blocks (F-CPU FBs marked with the safety attribute) are still hard-blocked in this mode — the runner refuses to start any test that targets a Safety block, regardless of preparation mode.

If the run fails immediately with "S7 connection succeeded but access denied — verify username/password", the PLC accepted the TLS handshake but rejected the credentials. Re-check the user/password in the suite config and confirm the matching entry under Vault → S7 Password. The runner reports this at connect time so the test does not fail later with a misleading "tag not found".

Batch runs and Real PLC suites: When you use Run All Test Suites or otherwise trigger a batch run that contains one or more suites configured for Real PLC mode, those suites are skipped (the per-suite acknowledgement dialog cannot be rendered from a batch). The Test Suites view shows a dismissible banner naming the skipped suites; open each affected suite and run it individually to walk through its acknowledgement dialog. Skipped suites are no longer counted as failures in the batch summary.

CPU operating-state gate: Real PLC runs probe the CPU's operating state right after connect. If the CPU is in Stop, StartUp, Hold, Defect or any other non-RUN state, the runner aborts immediately with Cannot run tests against real PLC: CPU is in <STATE>, not Run. Switch the CPU to Run before retrying. This prevents the runner from writing to DB memory while the user program isn't executing — which would otherwise cause every test to read residual data and report misleading PASS/FAIL outcomes. Set the CPU to RUN (TIA Portal Online & Diagnostics, or the CPU mode-selector switch) and run again.

Running Against a Protected Project

If your TIA project has "Protect confidential PLC configuration data" enabled, the automatic TCP download mode requires the project's master-secret password:

  1. Set the preparation mode to Automatic TCP download (tiaTcpDownload) in the suite config
  2. Supply the project's master-secret password when prompted
  3. Optional: choose Remember password to store it in the Windows Credential Manager, so every future run picks it up automatically without asking again. Otherwise the password is used once and forgotten after the run
  4. Run the suite from the Test Suites view

A hint "A password is stored for this user" appears next time if you saved the password. Leave the password field blank to keep the stored value. Clearing Remember password deletes the stored entry.

The password is never written to the suite file, to any log, or to any settings file. It lives only in Windows Credential Manager under the service name com.anyautomationstudio and is transferred from the UI to the runner through a user-bound DPAPI channel so a different Windows account cannot read it.

Managing Suites and Test Cases

  • Rename a suite by editing the JSON name field at the top of the suite and pressing Save — the file is renamed on disk to match (see Renaming a suite from the JSON name field). To delete a suite, remove its .json file from the .tia-tests/ folder.
  • Edit a test case in the Visual mode of the Test Suite Editor: each case row lets you rename it, edit its description, Duplicate it, remove it, and reorder its multi-phase steps with Move Up / Move Down.
  • Edit the generated SCL test code in the SCL view of the Test Suite Editor: rename test cases, change the input value assignments, edit, add or remove assertion checks, and add or delete whole test-case blocks. Changes flow back into the test cases automatically — the Visual and JSON views show them immediately, and case details the SCL text does not show (description, cycles, multi-phase steps, tolerance, watch list, tags, priority, owner, requirements) are kept. The runner block at the end is maintained automatically — edits to it have no effect. If the text no longer matches the test-block structure, an error message with the line number appears, and Save and Import to TIA are blocked until it is fixed. Switching to another editor tab discards SCL text that has not been applied yet; changes already applied are kept.
  • Connection settings are per suite. The transport, PLCSim instance name, IP address, cycle wait time, S7 Comm+ target (IP, port, user, password), and auto-connect behaviour all live in the suite file's config block. They are saved with the suite, so the next time you open it the same values are in effect. Passwords continue to live in the Windows Credential Manager — the suite file only stores a reference, never a plaintext password. (A Connection settings… form to edit these without touching JSON is planned — see Choosing a Transport.)
  • Before running, Studio checks that every variable name in your suite exactly matches the block interface, including case. If any mismatches are found, a warning dialog shows the suggested corrections — you can still continue the run after acknowledging it.
  • When a test case fails, its error message appears under the case name in the Test Results view (and on hover), and the detail grid marks the failing assertion with a red ✗ alongside its Expected and Actual values. (Jumping from a failed case straight to that variable in the Test Suite Editor is planned.)
  • If a suite file is modified externally — or renamed via the JSON name field above — the open editor automatically reloads, unless you have unsaved changes, in which case your edits are preserved.

Troubleshooting

  • Status icons don't update after a run: Make sure the suite file path is correct (absolute path under .tia-tests/). New unsaved suites get a unique path per click.
  • Run action unavailable: A TIA project must be open, and the active suite document must be valid JSON.
  • Test Explorer is empty: The working directory has no .tia-tests/ folder yet. Click New Test Suite to create the first one — the folder is created automatically.
  • PLCSIM errors: Check that PLCSIM Advanced V3.0+ is installed and licensed. The exact version is auto-detected at runtime.
  • Capture the TLS traffic for support (advanced): If support asks for a Wireshark capture of the S7 Comm+ handshake, set the SSLKEYLOGFILE environment variable to an absolute file path before starting the app — for example setx SSLKEYLOGFILE "C:\Temp\s7-keys.log" — then restart. Point Wireshark's TLS preference (Pre)-Master-Secret log filename at the same file. A warning is written to the app log on every connection while this is enabled so you don't forget to unset it. Leave the variable empty in normal operation.

AI-Authored Test Suites (Enterprise)

AI Chat can draft, refine and (optionally) run full SCL unit-test suites for you. Requires an Enterprise license.

Write Unit Tests skill (main chat)

  1. Open AI Chat and pick the Write Unit Tests skill from the skill picker (🧪 icon), or simply ask: "write unit tests for FB_MotorControl"
  2. The assistant reads the block interface, computes boundary values, plans test cases and summarises the plan in the chat — no giant JSON blobs
  3. When it's ready to write the suite, an Approve / Deny prompt appears inline. Approve to let the assistant save the suite to .tia-tests/
  4. If you asked the assistant to also run the suite, a second approval prompt appears for the run itself. Results come back as a pass/fail summary in the chat, and the assistant offers to refine any failing cases
  5. The Test Suites view picks up the new suite automatically — open it, review it in the Visual mode or as JSON Text, and run it yourself whenever you want
  6. You can also ask the assistant to "open that suite" after it's been created — the suite opens in the Test Suite Editor without you having to switch to the Test Suites view

Inline chat in the suite editor (Ctrl+I)

While a test suite is open in the JSON editor:

  1. Place the cursor near the test case you want to change (or select a range)
  2. Press Ctrl+I — a small prompt box appears above the editor
  3. Type what you want, for example:
    • "add an overflow test for Counter_Value"
    • "make this test parametrized with five speed setpoints"
    • "tighten the tolerance on all REAL assertions to 0.01"
  4. The assistant streams the updated JSON and a diff overlay appears in the editor
  5. Click Accept to apply the change (the file is saved and the Test Explorer reloads) or Reject to discard

The inline chat uses the same block interface the suite was opened with, so variable names and types are validated against the real block — mismatches are caught before you save.

AI badge in the Test Explorer and Visual editor

TestCases authored by the AI carry a small AI tag that appears:

  • next to the test case name in the Visual editor
  • in the Test Explorer tree

Review AI-authored test cases just like you would a colleague's pull request — read each assertion, sanity-check expected values, then run on the simulator before pointing it at real hardware.

Safety (F-CPU) protection

AI test authoring is gated for Safety blocks:

  • Creating tests — The assistant refuses to author tests for an F-CPU block without an explicit, in-conversation confirmation from you. The underlying tools also reject the write if the confirmation is missing
  • Running tests — The assistant cannot run tests against Safety blocks. Full stop, no override. Safety-block verification requires a certified methodology (TÜV/CE), not an AI-generated run. If you want to run a test against a Safety block, trigger the run yourself from the Test Explorer

Licensing

The Write Unit Tests skill and the unit-test MCP tools are Enterprise-only. On Basic or Professional tiers, the skill surfaces an "Enterprise required" message before any AI call is made.

CI/CD Integration

The tia-test-runner command-line tool runs your SCL unit-test suites on a build server — no UI, no manual clicks. Point it at a TIA Portal project, and it discovers the suites in .tia-tests/, runs them, writes machine-readable reports, and returns an exit code your pipeline can act on. This is an Enterprise feature; the same license rules as the in-app workspace apply (see Licensing for CI below).

The tia-test-runner command-line tool

The tool ships as tia-test-runner.exe and exposes six verbs:

Verb Purpose
run Execute a unit-test run against a TIA Portal project and write reports
compare Compare two previous runs and emit a diff report
trend Export historical pass/fail trends to CSV
validate Check test-suite definitions for schema errors without running them
verify Verify the integrity manifest of a previous run (detect tampered or missing report files)
version Print the runner, bridge, engine, build, .NET and operating-system versions

A typical run:

tia-test-runner run ^
  --project "C:\Projects\Plant.ap20" ^
  --plc PLC_1 ^
  --suite-filter * ^
  --out-dir reports

Key flags for run:

Flag Meaning
--project <PATH> TIA Portal project file (.ap20, .ap21, …). Required.
--out-dir <PATH> Directory for reports and logs. Required.
--plc <NAME> Restrict the run to a single PLC station.
--suite-filter <PATTERN> Glob filter on suite names (*, ?, literal).
--tag <TAG> Run only cases carrying this tag (repeatable).
--priority-min <LEVEL> Minimum priority (Low / Normal / High / Critical).
--report-format <FORMAT> junit, html, or json (repeatable). Defaults to JUnit + HTML.
--rerun-affected Run only suites whose underlying block changed since the last run (needs --plc).
--fail-fast Stop at the first failing case.
--timeout-minutes <N> Per-run timeout. Defaults to 60.
--attach Attach to a TIA Portal instance that is already running instead of opening the project file.
--config <PATH> Read defaults from a YAML configuration file (see below).

Exit codes (the pipeline contract):

Code Meaning
0 All tests passed
1 One or more tests failed
2 Run error (could not start, lost the PLC connection, license not entitled, …)
3 Argument or configuration error
4 Timeout
5 Cancelled by the user (Ctrl+C)

After a run, --out-dir holds a JUnit XML file your CI test reporter can publish, a self-contained branded HTML report, and an integrity manifest. Reports are append-only: a second run into the same directory keeps the earlier run and adds an aggregate summary.

Report classnames

By default, JUnit classname values follow the pattern {PlcName}.{BlockName}, which most CI test viewers group into a readable tree. Override it with --report-classname-pattern using any combination of these placeholders:

  • {PlcName} — the PLC/CPU name
  • {BlockName} — the block under test
  • {SuiteName} — the test-suite name
  • {ProjectName} — the project name

Example: --report-classname-pattern "{ProjectName}/{PlcName}/{SuiteName}".

Capturing CI environment details

Pass --ci-env-capture to record a small, fixed set of CI variables into the run's provenance, so the reports show which pipeline, commit, and agent produced each run. Capture is opt-in — nothing is recorded unless you ask for it. The tool recognises GitHub Actions, Jenkins, Azure DevOps, and GitLab CI (and a generic CI marker); only a whitelisted set of identifiers (run/build IDs, commit SHA, branch/ref, workflow/job name, agent/runner name) is read, each clipped to a safe length.

YAML configuration file

To keep long command lines out of your pipeline, put the defaults in a YAML file and pass --config <PATH>. Command-line flags override the file. Example tia-tests.yml:

project: C:\Projects\Plant.ap20
plc: PLC_1
out_dir: reports
report_format:
  - junit
  - html
timeout_minutes: 45
ci_env_capture: true

Jenkins (declarative pipeline)

pipeline {
  agent { label 'tia-windows' }
  stages {
    stage('Checkout') {
      steps { checkout scm }
    }
    stage('Unit Tests') {
      steps {
        bat 'tia-test-runner run --project "%WORKSPACE%\\Plant.ap20" --plc PLC_1 --out-dir reports --ci-env-capture'
      }
    }
  }
  post {
    always {
      junit 'reports/**/junit.xml'
      archiveArtifacts artifacts: 'reports/**/*.html', allowEmptyArchive: true
    }
  }
}

GitHub Actions

name: TIA Unit Tests
on: [push]
jobs:
  unit-tests:
    runs-on: [self-hosted, windows, tia]
    steps:
      - uses: actions/checkout@v4
      - name: Run SCL unit tests
        run: tia-test-runner run --project "${{ github.workspace }}\Plant.ap20" --plc PLC_1 --out-dir reports --ci-env-capture
      - name: Publish test report
        if: always()
        uses: dorny/test-reporter@v1
        with:
          name: TIA Unit Tests
          path: reports/**/junit.xml
          reporter: java-junit
      - name: Upload HTML report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: tia-html-report
          path: reports/**/*.html

Azure DevOps

pool:
  name: tia-windows
steps:
  - checkout: self
  - script: tia-test-runner run --project "$(Build.SourcesDirectory)\Plant.ap20" --plc PLC_1 --out-dir $(Build.ArtifactStagingDirectory)\reports --ci-env-capture
    displayName: Run SCL unit tests
  - task: PublishTestResults@2
    condition: always()
    inputs:
      testResultsFormat: JUnit
      testResultsFiles: '$(Build.ArtifactStagingDirectory)/reports/**/junit.xml'
      testRunTitle: TIA Unit Tests
  - publish: $(Build.ArtifactStagingDirectory)\reports
    artifact: tia-html-report
    condition: always()

GitLab CI

unit-tests:
  tags: [tia-windows]
  script:
    - tia-test-runner run --project "$CI_PROJECT_DIR\Plant.ap20" --plc PLC_1 --out-dir reports --ci-env-capture
  artifacts:
    when: always
    paths:
      - reports/
    reports:
      junit: reports/**/junit.xml

Generate a CI/CD pipeline from the app

Planned for the in-app workspace. An in-app generator for these files is planned. Until it ships, copy one of the templates above. The intended flow:

Instead of writing the configuration files by hand, you would let AnyAutomation Studio generate them for you. The generator reads the open project and produces two ready-to-commit files: a runner.yaml that holds the command-line defaults, and a pipeline file for the CI system you choose.

The intended flow:

  1. Open a project.
  2. Open the Test Suites view.
  3. Run Generate Pipeline….
  4. Pick your CI system (GitHub Actions, Jenkins, Azure DevOps, or GitLab CI) and adjust the prefilled fields — project path, PLC name, an optional suite filter, the report output folder, which report formats to write, an optional self-hosted runner label, and optional report branding.
  5. Generate Preview to see both files side by side.
  6. Write Files to save them. If a file already exists, you are asked before it is overwritten.

The runner.yaml is written to the project folder, and the pipeline file is written to the path its CI system expects: .github/workflows/tia-tests.yml for GitHub Actions, Jenkinsfile for Jenkins, azure-pipelines.yml for Azure DevOps, and .gitlab-ci.yml for GitLab CI. Commit both files. The generated pipeline runs on demand — start it manually from your CI system whenever you want to test, rather than automatically on every push.

The pipeline must run on a self-hosted Windows runner. The tests drive TIA Portal and PLCSIM, so the machine that executes the pipeline needs TIA Portal installed, PLCSIM available, and an active Enterprise license. Cloud-hosted runners cannot run the tests. Enter the label of your own Windows runner in the Self-hosted runner label field so the generated pipeline targets it; leave the field empty only if you have already wired up a self-hosted runner by other means.

Licensing for CI

Unit testing is an Enterprise feature, so the runner checks the license before it does any work. There are three ways to license a CI agent:

  1. Machine-bound (default) — the agent uses the license already activated on that machine, exactly like the desktop app. Best for a dedicated, long-lived build server.
  2. Per-run key — pass --license-key <KEY> to activate at the start of the run. The key is masked in all logs.
  3. Offline (locally cached) — pass --license-offline <PATH> on agents with limited network access. The runner does not attempt an online activation; instead it relies on the locally cached, signed license.

Offline limitation — please read. The offline mode is a cached, grace-tolerant license, not a fully air-gapped activation. It works for roughly 14 days after the last successful online validation. An agent that never reaches the network again loses its entitlement once that grace window expires, and the run then fails with exit code 2. To keep an offline agent green, let it validate online periodically (for example on a scheduled job). A dedicated offline activation-file import is planned for a future release; until then, the path you pass to --license-offline is noted in the log and the cached/grace license is used.

Proxy configuration

The runner honours the standard proxy environment variables by default — set HTTPS_PROXY, HTTP_PROXY, and NO_PROXY on the build agent and online license validation will route through your proxy. No extra flags are needed.

Supported environments

Environment Supported? Notes
Windows 11 (desktop) Yes Reference environment.
Windows Server 2022 with Desktop Experience Yes TIA Portal and PLCSIM Advanced require an interactive desktop session.
Windows Server Core No TIA Portal Openness needs the full desktop shell; the headless Server Core install does not provide it.
Docker / Windows container No TIA Portal and PLCSIM Advanced are not supported inside containers.

Troubleshooting CI runs

Symptom Cause and fix
Exit code 3, "--project is required" A required argument is missing or the YAML config could not be read. Check --project, --out-dir, and --config paths.
Exit code 2, license message The agent has no valid entitlement. Activate a machine-bound license, pass --license-key, or — for offline agents — make sure the cached license validated online within the last ~14 days.
Exit code 2, "could not write to output directory" --out-dir points at a protected system location. Use a path under the project directory or under the agent's local app-data area.
Exit code 4, timeout The run exceeded --timeout-minutes. Raise the limit, or split large suites; check that the PLC connection is reachable from the agent.
No test report appears in CI The test-reporter step is not pointed at junit.xml, or it only runs on success. Publish reports with an always()/when: always condition and point it at reports/**/junit.xml.
Online validation hangs behind a corporate proxy Set HTTPS_PROXY / HTTP_PROXY (and NO_PROXY for internal hosts) on the agent.

Privacy

No telemetry is collected by the CLI or the UI. The CI variables surfaced in reports are recorded only when you opt in with --ci-env-capture, and the project path is stored as an irreversible hash, never in plain text. HTML reports meet WCAG 2.1 AA, verified by automated checks; a manual screen-reader (NVDA) sign-off is part of the release end-to-end pass.