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.
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
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).
Testsuites are function blocks implementing IUnitTest
FUNCTION_BLOCK <POU>Test EXTENDS <POU> IMPLEMENTS Testbench.IUnitTestIf 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.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 (
Adderunder test,AdderTesttest 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
assertionsinput provides helpers such asEqualsInt,IsTrue, andEqualsArray2dLreal.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_METHODVariant 2: Context (multi-cycle)
Use when the test must run across several PLC scan cycles (timers, state machines, waiting for
Done). Pass ITestContext ascontext. Assert viacontext.Assertions. Setcontext.Busy := TRUEwhile the test still needs cycles; set it toFALSEwhen finished. The method is called every cycle untilBusyisFALSE. The same context is kept for the whole test suite, socontext.UserDatacan 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_METHODVariant 3: Expected / actual (single check)
Use for the smallest possible test: one comparison with no assertion API. Assign
expected,actual, and optionallymessage; the framework comparesexpectedandactual(any IEC 61131-3 type such asBOOL,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_METHODTest may be parametrized by using the
DataRowattribute. 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_VARFor parameterized tests that span multiple cycles, use
contextinstead ofassertions:{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