Here you’ll find the most commonly asked questions and their answers. If you don’t find what you are looking for here, you can look through the:
- Open and closed issues on GitHub (TcUnit)
- Open and closed issues on GitHub (TcUnit-Runner)
- Discussions on GitHub
TcUnit
This can be accomplished by keeping the function block under test as an instance variable of the test suite rather than the test method. You can download an example here. In this example, the FB_ToBeTested
is instantiated under the test suite (FB_ToBeTested_Test
), and can thus be controlled over multiple cycles. Then all that’s necessary to do is to set the condition for when the assertion should be made in the test itself, which in the example is when the TestSuiteTimer
has elapsed (TestSuiteTimer.Q
).
Required TcUnit version: 1.0 or later
In a number of scenarios, TwinCAT won’t let you write directly to certain variables:
- Due to access restrictions (e.g. a variable in a FB’s VAR)
- The variable being set as I/O (i.e.
AT %I*
orAT %Q*
)
Writing to these variables wouldn’t make sense and should be prevented in the normal PLC code, so having special privileges during testing is a must.
To support these cases, TcUnit provides helper functions like WRITE_PROTECTED_BOOL()
, WRITE_PROTECTED_INT()
(and so forth) for setting these type of variables. For an example of how to use these, let’s assume you have a test:
METHOD PRIVATE TestCommsOkChannelsLow VAR EL1008 : FB_Beckhoff_EL1008; END_VAR
Where the FB_Beckhoff_EL1008 holds a variable:
iChannelInput AT %I* : ARRAY[1..8] OF BOOL;
Now you might want to write a value to the first channel of the iChannelInput like:
TcUnit.WRITE_PROTECTED_BOOL(Ptr := ADR(EL1008.iChannelInput[1]), Value := FALSE);
Whereas afterwards you can make an assertion as usual:
AssertFalse(Condition := EL1008.ChannelInput[1], Message := 'Channel is not false');
Required TcUnit version: 1.0 or later
You can accomplish this by the Hide reference option for referenced libraries. This option lets you hide TcUnit from your other projects.
Let’s assume you’ve developed a library MyLibrary
, which has tests written in TcUnit. You make a PLC project MyProject
, which references MyLibrary
.
If you use Hide reference on TcUnit in MyLibrary
, then TcUnit won’t show up in the imports list of MyProject
. You can find it in the Properties tab:
I want to do an assertion on two variables both declared with the BIT
-datatype, but I have noticed that a AssertEquals_BIT()
does not exist. What do I do?
The reason a AssertEquals_BIT()
does not exist is that TwinCAT does not allow a BIT
-datatype as a variable input. If you have data declared with the BIT
-type, the easiest way to do an assertion on these is to do a BIT_TO_BOOL()
conversion and use the AssertEquals_BOOL()
.
TEST('Testing_of_BIT_Type'); AssertEquals_BOOL(Expected := BIT_TO_BOOL(VariableDeclaredAsBit_A), Actual := BIT_TO_BOOL(VariableDeclaredAsBit_B), Message := 'The variables differ'); TEST_FINISHED();
Required TcUnit version: 1.0 or later
When TcUnit is running it allocates memory in the PLC to store the test results. The maximum number of tests for every test suite has been set to 100, which however is a configuration parameter for TcUnit and can be changed. Parameters for TcUnit (and in fact any library references) are stored in your project, which means that this change will be persistent for your project/library. To change this max amount, to say for instance 200 tests per test suite, go to the library references and select TcUnit, then go to GVLs
→ GVL_Param_TcUnit
→ MaxNumberOfTestsForEachTestSuite
.
Required TcUnit version: 1.0 or later
Yes. By default TcUnit runs all the test suites and tests in parallel, in other words all test suites and tests are run at the same time. Sometimes it is however desirable to run either the test suites or tests (or both) in a sequence, for example if you get exceed overruns while running tests. Since TcUnit 1.2 it’s possible to run test suites in sequence (one after another) and/or individual tests in order (one after another).
To execute test suites in a sequence, simply replace TcUnit.RUN()
with TcUnit.RUN_IN_SEQUENCE()
in your main body of the test program. This will execute the test suites in the order that they were declared. So for example if we have defined the following test suites and test program:
PROGRAM PRG_TEST VAR fbDiagnosticMessageDiagnosticCodeParser_Test : FB_DiagnosticMessageDiagnosticCodeParser_Test; fbDiagnosticMessageFlagsParser_Test : FB_DiagnosticMessageFlagsParser_Test; fbDiagnosticMessageParser_Test : FB_DiagnosticMessageParser_Test; END_VAR TcUnit.RUN_IN_SEQUENCE();
This will first execute all tests defined in fbDiagnosticMessageDiagnosticCodeParser_Test
, once all tests are finished in that function block, TcUnit will execute all tests in fbDiagnosticMessageFlagsParser_Test
, and when that is done it will execute all tests in fbDiagnosticMessageParser_Test
.
It’s also possible to execute individual tests in order by simply replacing TEST('TestName')
with TEST_ORDERED('TestName')
. This will execute the tests in the order that the TEST_ORDERED()
is called for the various tests. TEST_ORDERED()
returns a boolean to indicate whether the TcUnit framework will run the test, so in order to only execute the code when it’s time for that particular test, it makes sense to check if TEST_ORDERED()
returns true, and only then do the execution of the function blocks and assertions, for example like this:
METHOD PRIVATE TestWithTimestampZeroTimeExpectCurrentTime VAR ... (variable declaration used for the test) END_VAR IF TEST_ORDERED('TestWithTimestampZeroTimeExpectCurrentTime') THEN fbFunctionBlockUnderTest(Parameters); AssertEquals(Expected := 'SomeValue', Actual := fbFunctionBlockUnderTest.OutVariable, Message := 'Test failed'); TEST_FINISHED(); END_IF
As usual, the TEST_FINISHED()
will indicate that this test is finished, and the framework will go to the next test. Note that you don’t need to create any state machine for calling the different TEST_ORDERED()
tests. You can (and must!) call all TEST_ORDERED()
at the same time. The framework will make sure to only care about the assertions of the test that is currently running.
This means the following combinations can be used:
RUN()
with all tests asTEST()
– means all tests suites and tests will run in parallel, this is the default behaviour in all previous versions of TcUnit
RUN_IN_SEQUENCE()
with all tests asTEST()
– means all test suites will run in sequence, but the tests in every test suite will run in parallel
RUN()
with all tests asTEST_ORDERED()
– means all test suites will run in parallel, but the tests in every test suite will run in sequence
RUN_IN_SEQUENCE()
with all tests asTEST_ORDERED()
– means all test suites will run in sequence, as will every test
For maximum flexibility, these combinations are allowed as well for special occasions:
TcUnit.RUN()
with mixed tests ofTEST()
andTEST_ORDERED()
– means all tests suites will run in parallel, with tests marked asTEST()
run in parallel with tests that are marked withTEST_ORDERED()
that run in sequence (relative to each other)TcUnit.RUN_IN_SEQUENCE()
with mixed tests ofTEST()
andTEST_ORDERED()
– means all test suites will run in sequence, with tests marked asTEST()
run in parallel with tests that are marked withTEST_ORDERED()
that run in sequence (relative to each other)
If you run tests with both TEST()
and TEST_ORDERED()
, all tests defined with TEST()
will run in parallel with the tests that are TEST_ORDERED()
.
Note that you can’t execute test-suites with both TcUnit.RUN()
and TcUnit.RUN_IN_SEQUENCE()
at the same time (which wouldn’t make any sense).
For a couple of TwinCAT projects that shows how to run both test suites in a sequence and individual tests in order, click here.
Required TcUnit version: 1.2 or later
If you have many test suites and/or tests, it can take some time for TcUnit to print all those results. Since version 1.1 of TcUnit, much more data is printed to the ADS-logger as this data is used for the communication with TcUnit-Runner. If you know that you will only run your tests locally and without integration to a CI/CD tool using TcUnit-Runner, you can set the parameter LogExtendedResults
to FALSE
(it is default TRUE
). To change this parameter, go to the library references and select TcUnit, then go to GVLs
→ GVL_Param_TcUnit
→ LogExtendedResults
.
Required TcUnit version: 1.1 or later
Yes. You can set the parameter TimeBetweenTestSuitesExecution
to whatever delay you want to have. To change this parameter, go to the library references and select TcUnit, then go to GVLs
→ GVL_Param_TcUnit
→ TimeBetweenTestSuitesExecution
. Default this parameter is set to T#0S
(zero seconds, i.e. no delay). For example, in the below screenshot this is changed to 5 seconds.
Required TcUnit version: 1.2 or later
If I call Tc2_System.ADSLOGSTR()
during execution of a test, my messages don’t arrive in the expected order. Let’s for example assume this very simple (always failing) test:
TEST('Test1'); FOR nCounter := 1 TO 5 BY 1 DO Tc2_System.ADSLOGSTR(msgCtrlMask := ADSLOG_MSGTYPE_HINT, msgFmtStr := 'Test Number is %s', strArg := INT_TO_STRING(nCounter)); sAssertionMessage := Tc2_Standard.CONCAT(STR1 := 'Assertion failed no. ', STR2 := INT_TO_STRING(nCounter)); AssertEquals_BOOL(Expected := TRUE, Actual := FALSE, Message := sAssertionMessage); END_FOR TEST_FINISHED();
This will result in this order of messages:
I want the messages to arrive in the correct order, how is this achieved?
The reason for this behavior is because all ADS messages that TcUnit creates/outputs are buffered (for deeper technical description read this). If Tc2_System.ADSLOGSTR()
is used directly, the messages can come out of sequence in relation to the message created by TcUnit as TcUnit buffers the messages to not overflow the ADS message router.
The solution is to use TCUNIT_ADSLOGSTR()
, which accepts the exact same inputs as ADSLOGSTR()
. By using this function, the ADSLOGSTR()
messages are put in the same buffer as TcUnit is using for its output.
So if we replaced the call to Tc2_System.ADSLOGSTR()
to TCUNIT_ADSLOGSTR()
instead, we get this:
Required TcUnit version: 1.2 or later
It’s done almost identical as in the introduction user guide, but simply replace the instance of the function block that you want to test with the call to the function instead.
Assume we have a function:
FUNCTION F_Sum VAR_INPUT one : UINT; two : UINT; END_VAR F_Sum := one + two;
Then the test would look like following:
METHOD TwoPlusTwoEqualsFour VAR Result : UINT; ExpectedSum : UINT := 4; END_VAR TEST('TwoPlusTwoEqualsFour'); Result := F_Sum(one := 2, two := 2); AssertEquals(Expected := ExpectedSum, Actual := Result, Message := 'The calculation is not correct'); TEST_FINISHED();
When running TcUnit with a controller using ARMv7 you can run into issues, such as breakpoints not working. This seems to be an issue with the limited memory of the controllers using an ARMv7 such as the CX8190 and CX9020. Please adjust the parameters related to memory allocation.
For more information on a set of working parameters, see this issue on GitHub.
TcUnit-Runner
No, Jenkins is just used as one example of an automation server. Any automation server of your choice should work fine, as long as it can execute the LaunchTcUnit.bat
windows batch script or the TcUnit-Runner.exe
windows executable, and it can handle standard xUnit/JUnit XML format it should work just fine. Here is an example configuration for a GitHub action and one for Azure pipelines
Required TcUnit version: 1.1 or later
Check the console output in Jenkins if it provides any information. Also, try to build the project manually (by opening Visual Studio) on the build server and check if builds correctly.
Yes, this is possible with the limitation that it is not possible to run TwinCAT on a Windows/shared core but instead it is required that TwinCAT (and more specifically the unit-tests) are to be run on an isolated core in the virtual machine. Go here to read more on the subject.
Required TcUnit version: 1.0 or later