Table of Contents

Test Explorer

Test Explorer is a tool provided by the Zeugwerk Creator developed to enhance the testing experience for TwinCAT projects. Tailored specifically for Beckhoff's TwinCAT automation platform, this tool seamlessly integrates with Visual Studio, providing a user-friendly interface to discover and execute tests directly within the TwinCAT development environment.

The primary goal of Test Explorer is to simplify the testing workflow for PLC developers by offering clear visibility into test results, test case organization, and execution status, all from within the IDE. Whether you are building complex automation systems or verifying critical control logic, TwinCAT Test Explorer helps ensure that your code remains robust, reliable, and easy to maintain.

image

Usage

Testexplorer automatically discovers tests, which are implemented with the Testbench library. This is a lightweight library designed to provide a unified interface for various PLC unittest framework implementations. It acts as a bridge, allowing developers to write consistent test logic while supporting multiple underlying test frameworks. It reduces the overhead to write tests and therefore lower the hurdle for writing unittests, to a bare minimum!

Implement Unittests

  1. Add Testbench to your PLC by including it as a dependency. The simplest way todo this is by using Twinpack Package Manager, alternatively you can grab the latest release from GitHub and install it manually in the TwinCAT XAE).

  2. Testsuites are function blocks implementing IUnitTest

    FUNCTION_BLOCK <POU>Test EXTENDS <POU> IMPLEMENTS Testbench.IUnitTest
    

    If prefered, it is of course also possible to add the <POU> as an instance to the function block. However, due to limitations of structured text, it can be helpful to actually extend from the object to get access protected methods of the POU.

  3. Tests are implemented as methods on each testsuite. Test Explorer recognizes three method signatures. Every test method name must start with Test_.

    Working examples for all three variants live in the Testbench Examples PLC (Adder under test, AdderTest test suite).

    Variant 1: Assertions (stateless)

    Use when the test can finish in a single call: pure logic, no timers, no state that must advance over PLC cycles. The assertions input provides helpers such as EqualsInt, IsTrue, and EqualsArray2dLreal.

    METHOD Test_Add_ReturnsCorrectResult
    VAR_INPUT
      assertions : Testbench.IAssertions;
    END_VAR
    VAR
      result : INT;
    END_VAR
    
    // arrange
    
    // act
    result := _adder.AddInt(5, 3);
    
    // assert
    assertions.EqualsInt(8, result, 'Result is correct');
    END_METHOD
    

    Variant 2: Context (multi-cycle)

    Use when the test must run across several PLC scan cycles (timers, state machines, waiting for Done). Pass ITestContext as context. Assert via context.Assertions. Set context.Busy := TRUE while the test still needs cycles; set it to FALSE when finished. The method is called every cycle until Busy is FALSE. The same context is kept for the whole test suite, so context.UserData can share data between methods.

    METHOD Test_CountsToTen
    VAR_INPUT
      context : Testbench.ITestContext;
    END_VAR
    VAR_INST
      counter : INT;
    END_VAR
    
    counter := counter + 1;
    
    context.Assertions.IsFalse(FALSE, '');
    
    context.Busy := counter < 10;
    
    IF NOT context.Busy
    THEN
      context.Assertions.EqualsInt(10, counter, 'Counted up to 10');
    END_IF
    END_METHOD
    

    Variant 3: Expected / actual (single check)

    Use for the smallest possible test: one comparison with no assertion API. Assign expected, actual, and optionally message; the framework compares expected and actual (any IEC 61131-3 type such as BOOL, INT, DINT, WORD, …).

    METHOD Test_Add_ReturnsCorrectResult_Output
    VAR_OUTPUT
      expected : INT;
      actual : INT;
      message : STRING(255);
    END_VAR
    
    expected := 8;
    actual := _adder.AddInt(5, 3);
    message := 'Result is correct';
    END_METHOD
    

    image

  4. Test may be parametrized by using the DataRow attribute. When generating the unittest, every datarow will be its own test. The signature for using paramtrized tests looks as follows.

    {attribute 'DataRow(<PARAMETER_1 VALUE>, <PARAMETER_2 VALUE>, ..., <PARAMETER_N VALUE>)'}
    {attribute 'DataRow(<PARAMETER_1 VALUE>, <PARAMETER_2 VALUE>, ..., <PARAMETER_N VALUE>)'}
    // ...
    {attribute 'DataRow(<PARAMETER_1 VALUE>, <PARAMETER_2 VALUE>, ..., <PARAMETER_N VALUE>)'}
    METHOD Test_<NAME OF THE TEST>
    VAR_INPUT
      assertions : IAssertions;
      <PARAMETER_1> : <PARAMETER_1 DATATYPE>
      <PARAMETER_2> : <PARAMETER_2 DATATYPE>
      // ...
      <PARAMETER_N> : <PARAMETER_N DATATYPE>
    END_VAR
    

    For parameterized tests that span multiple cycles, use context instead of assertions:

    {attribute 'DataRow(<PARAMETER_1 VALUE>, <PARAMETER_2 VALUE>, ..., <PARAMETER_N VALUE>)'}
    {attribute 'DataRow(<PARAMETER_1 VALUE>, <PARAMETER_2 VALUE>, ..., <PARAMETER_N VALUE>)'}
    // ...
    {attribute 'DataRow(<PARAMETER_1 VALUE>, <PARAMETER_2 VALUE>, ..., <PARAMETER_N VALUE>)'}
    METHOD Test_<NAME OF THE TEST>
    VAR_INPUT
      context : Testbench.ITestContext;
      <PARAMETER_1> : <PARAMETER_1 DATATYPE>
      <PARAMETER_2> : <PARAMETER_2 DATATYPE>
      // ...
      <PARAMETER_N> : <PARAMETER_N DATATYPE>
    END_VAR
    

Execute Unittests

  • Navigate to Tools > Zeugwerk Creator > Test Explorer
  • In the new pane, click on Run All Tests, this will
    • Compile your PLC and check all objects
    • Install it as a local library
    • In the background create a new PLC containing the code generation for all tests (tests/Tests.sln)
    • Compile the Testing PLC
    • Activate it on a target

You can check the Examples PLC in the Testbench repository for a demonstration

image image