In the last post of this series we were looking at a use case for a certain set of functionality, more specifically creating parser function blocks for the handling of IO-Link events. The result was a series of function blocks with defined input and output. In this post we’ll create the unit tests that will use the function blocks that we’ve started doing. Naturally, when defining the tests they will all fail as we don’t have the implementation code ready yet.

As our IO-Link project is a library project and thus won’t have any runnable tasks to be running in any PLC, we still need to have a task and a program to run our unit tests. This task will be running on our local development machine. We need to create a task/program inside the library project which initiates all the unit tests, so we can run the unit tests which in turn initializes the library code that we want to test. I usually put all my unit tests in a folder called test next to the standard folder POUs, which are initialized by the program PRG_TEST. For every function block we’ve defined, we’ll create an accompanying function block unit test. I usually call this function block the same as the function block that we want to test, but add _test in the name.

Unit test structure

As can be seen here every FB that we’ve defined a functionality for has an accompanying test-FB. By structuring all your libraries in this way, all the function blocks and unit tests will always be available together. Also, by having the program/task available in the library project, any developer can at anytime run the unit tests. I think this is an excellent way to package everything nicely together.

Next, we instantiate every test-FB in PRG_MAIN, and every test-FB in turn instantiates the FB that it is supposed to test. The output of every test-FB is a single boolean bSuccess which returns whether running all unit tests for that particular FB has succeeded or not.

Unit test structure calls

Every unit test-FB (in red) can do as many tests for the FB under test (in green) as you want, i.e. make calls to the FBs with different inputs to test various scenarios. One example for a parser FB could be to test min/mid/max values as input, totaling to three different tests. Every test that we are going to run requires some sort of test fixture, which are the prerequisites for the test. In our case, this will be setting up the data bytes used as input for the function block under test. For every test that we will run we will have to do an assertion, checking whether the result (output) we get from our function block is equal to the expected result.

I would like to add an important side-note here. It’s at these moments that you want to have unit testing frameworks. Unit testing frameworks help you as a developer to use a common way of doing these assertions, and getting the results. With a unit test framework you could for example do an integration of a logging service, and log the results of the unit tests to either a file or to screen, for instance, by showing error-messages in the Visual Studio error list. By utilizing a common unit test framework and having it as a separate library, you can share it among the whole team of developers. For all my code I’ve done a TwinCAT unit test framework, which I will come back to in the future. Again I would like to remind that for these series of posts we’re not going to do a unit test framework, but rather show the concept of doing unit testing. In all the coming examples, the outputs (results) of our unit tests will be booleans which simply can be displayed in the online view of TwinCAT.

Let’s start by creating the five unit test FBs (red above), and instantiating them in PRG_TEST and running them.


    fbDiagnosticMessageDiagnosticCodeParser_Test : FB_DiagnosticMessageDiagnosticCodeParser_Test;
    bSuccess_DiagnosticMessageCodeParser : BOOL;
    fbDiagnosticMessageFlagsParser_Test : FB_DiagnosticMessageFlagsParser_Test;
    bSuccess_DiagnosticMessageFlagsParser : BOOL;
    fbDiagnosticMessageParser_Test : FB_DiagnosticMessageParser_Test;
    bSuccess_DiagnosticMessageParser : BOOL;
    fbDiagnosticMessageTextIdentityParser_Test : FB_DiagnosticMessageTextIdentityParser_Test;
    bSuccess_DiagnosticMessageTextIdentityParser : BOOL;
    fbDiagnosticMessageTimeStampParser_Test : FB_DiagnosticMessageTimeStampParser_Test;
    bSuccess_DiagnosticMessageTimeStampParser : BOOL;
    fbGetCurTaskIndex : GETCURTASKINDEX;


IF _TaskInfo[fbGetCurTaskIndex.index].FirstCycle THEN
    fbDiagnosticMessageDiagnosticCodeParser_Test(bSuccess => bSuccess_DiagnosticMessageCodeParser);
    fbDiagnosticMessageFlagsParser_Test(bSuccess => bSuccess_DiagnosticMessageFlagsParser);
    fbDiagnosticMessageParser_Test(bSuccess => bSuccess_DiagnosticMessageParser);
    fbDiagnosticMessageTextIdentityParser_Test(bSuccess => bSuccess_DiagnosticMessageTextIdentityParser);
    fbDiagnosticMessageTimeStampParser_Test(bSuccess => bSuccess_DiagnosticMessageTimeStampParser);

As these unit tests are simple and don’t need to be called several times (but only once), I utilize the built-in FirstCycle-boolean in TwinCAT3. If we built this program and run it on our local machine, we’ll get:

Unit test not called

All bSuccess-results are false. Probably not a big surprise, as we have neither implemented the unit test function block code nor the function blocks that they are supposed to test! What we need to do now is to implement each unit test-FB with some tests that we think should be included for each parser.

FB_DiagnosticMessageDiagnosticCodeParser_Test Link to heading

As explained in the previous post of this series, the function block FB_DiagnosticMessageDiagnosticCodeParser is responsible for parsing a diagnostic code type (ManufacturerSpecific, EmergencyErrorCode or ProfileSpecific) together with the code itself.

FUNCTION_BLOCK FB_DiagnosticMessageDiagnosticCodeParser_Test
    bSuccess : BOOL;
    fbDiagnosticMessageDiagnosticCodeParser : FB_DiagnosticMessageDiagnosticCodeParser;
    stDiagnosticCode : ST_DIAGNOSTICCODE;
    bSuccess_fbDiagnosticMessageDiagnosticCodeParser_EmergencyErrorCode : BOOL;
    bSuccess_fbDiagnosticMessageDiagnosticCodeParser_ManufacturerSpecific : BOOL;
    bSuccess_fbDiagnosticMessageDiagnosticCodeParser_ProfileSpecific : BOOL;
    bSuccess_fbDiagnosticMessageDiagnosticCodeParser_ReservedForFutureUse : BOOL;

The total success of the unit test-FB is bSuccess, but each individual test case that we will be running inside this function block will have its own bSuccess-variable. In this case, if the tests fail, we’ll be able to see which specific test that has failed. The stDiagnosticCode variable is the output of the FB that we’ll be testing, and this is thus the one that we will compare the expected result with.

Our next step is setting up all test fixtures and the expected test results. Here we need to think about the different test cases that we would like to run. We want to make sure our function block correctly parses the three different diagnostic code types, but we also want to make sure our code correctly handles the case when the IO-Link master for instance outputs a diagnosis code type that’s not valid (Reserved for future use for instance). In this case we want to make sure our function block sets it to “Unspecified”. To make sure that our code handles all these different cases, we want four good tests.

The first test fixture will represent an emergency code, and will look like follows:

    // @TEST-FIXTURE EmergencyErrorCode
    cnDiagnosticCodeBufferByte1_EmergencyErrorCode : BYTE := 16#00; // 16#E800
    cnDiagnosticCodeBufferByte2_EmergencyErrorCode : BYTE := 16#E8;
    cnDiagnosticCodeBufferByte3_EmergencyErrorCode : BYTE := 16#30; // 16#7530 = 10#30000
    cnDiagnosticCodeBufferByte4_EmergencyErrorCode : BYTE := 16#75;
    canDiagnosticCodeBuffer_EmergencyErrorCode : ARRAY[1..4] OF BYTE := [
    // @TEST-RESULT EmergencyErrorCode
    ceDiagnosticCodeType_EmergencyErrorCode : E_DIAGNOSTICCODETYPE := E_DIAGNOSTICCODETYPE.EmergencyErrorCodeDS301;
    cnDiagnosticCode_EmergencyErrorCode : UINT := 10#30000;

First, notice I put these as constants, just because this is simply what they are.

The four bytes for our test fixture represents (again reminding from previous post):

Bit 0-15 Bit 16-31
0x0000-0xDFFF not used
0xE000-0xE7FF can be used manufacturer specific
0xE800 Emergency Error Code as defined in DS301 or DS4xxx
0xE801-0xEDFF reserved for future standardization
0xEE00-0xEFFF Profile specific
0xF000-0xFFFF not used

We have two bytes for the type of diagnosis code, and two bytes for the code itself. Emergency error code is defined as 0xE800, and we set the code itself to 0x7530 (representing a decimal value of 30000).

Diagnosis code layout

We put these four bytes into a constant array, which we’ll be using as an input to the FB under test once we are going to run our tests. The test result for the struct that the function block outputs should be “EmergencyErrorCodeDS301” and 30000 (decimal), which is the result defined under the @TEST-RESULT.

Next we move to the body of the test-FB:

// @TEST-RUN EmergencyErrorCode
fbDiagnosticMessageDiagnosticCodeParser(anDiagnosticCodeBuffer := canDiagnosticCodeBuffer_EmergencyErrorCode,
                                        stDiagnosticCode => stDiagnosticCode);
// @TEST-ASSERT EmergencyErrorCode
bSuccess_fbDiagnosticMessageDiagnosticCodeParser_EmergencyErrorCode :=
    (stDiagnosticCode.eDiagnosticCodeType = ceDiagnosticCodeType_EmergencyErrorCode) AND
    (stDiagnosticCode.nCode = cnDiagnosticCode_EmergencyErrorCode);

What we’re doing here is to run the test by calling the function block with our test fixture (defined as constant array) as input, and checking whether the result we get in our output is according to our expected test-result. Next we set the bSuccess_fbDiagnosticMessageDiagnosticCodeParser_EmergencyErrorCode boolean to true if the output matches our expected result, and to false if it doesn’t. And that’s basically everything there is to it! If we were to run our code now, the test would fail simply because our function block doesn’t do anything as it has not yet been implemented! Before doing any implementation code, we need to finish our different test cases so we cover as much scenarios as possible. What follows are three additional test fixtures and expected test results.

// @TEST-FIXTURE ManuFacturerSpecific
cnDiagnosticCodeBufferByte1_ManufacturerSpecific : BYTE := 16#00; // 16#E000 (in range of 0xE000 - 0xE7FF)
cnDiagnosticCodeBufferByte2_ManufacturerSpecific : BYTE := 16#E0;
cnDiagnosticCodeBufferByte3_ManufacturerSpecific : BYTE := 16#E8; // 16#03E8 = 10#1000
cnDiagnosticCodeBufferByte4_ManufacturerSpecific : BYTE := 16#03;
canDiagnosticCodeBuffer_ManufacturerSpecific : ARRAY[1..4] OF BYTE := [
// @TEST-RESULT ManuFacturerSpecific
ceDiagnosticCodeType_ManufacturerSpecific : E_DIAGNOSTICCODETYPE := E_DIAGNOSTICCODETYPE.ManufacturerSpecific;
cnDiagnosticCode_ManufacturerSpecific : UINT := 10#1000;
// @TEST-FIXTURE ProfileSpecific
cnDiagnosticCodeBufferByte1_ProfileSpecific : BYTE := 16#10; // 16#EF10 (in range of 0xEE00 - 0xEFFF)
cnDiagnosticCodeBufferByte2_ProfileSpecific : BYTE := 16#EF;
cnDiagnosticCodeBufferByte3_ProfileSpecific : BYTE := 16#FF; // 16#FFFF = 10#65535
cnDiagnosticCodeBufferByte4_ProfileSpecific : BYTE := 16#FF;
canDiagnosticCodeBuffer_ProfileSpecific : ARRAY[1..4] OF BYTE := [
// @TEST-RESULT ProfileSpecific
ceDiagnosticCodeType_ProfileSpecific : E_DIAGNOSTICCODETYPE := E_DIAGNOSTICCODETYPE.ProfileSpecific;
cnDiagnosticCode_ProfileSpecific : UINT := 10#65535;
// @TEST-FIXTURE ReservedForFutureUse
cnDiagnosticCodeBufferByte1_ReservedForFutureUse : BYTE := 16#01; // 16#E801 (in range of 0xE801 - 0xEDFF)
cnDiagnosticCodeBufferByte2_ReservedForFutureUse : BYTE := 16#E8;
cnDiagnosticCodeBufferByte3_ReservedForFutureUse : BYTE := 16#D9; // 16#3BD9 = 10#15321
cnDiagnosticCodeBufferByte4_ReservedForFutureUse : BYTE := 16#3B;
canDiagnosticCodeBuffer_ReservedForFutureUse : ARRAY[1..4] OF BYTE := [
// @TEST-RESULT ReservedForFutureUse
ceDiagnosticCodeType_ReservedForFutureUse : E_DIAGNOSTICCODETYPE := E_DIAGNOSTICCODETYPE.Unspecified;
cnDiagnosticCode_ReservedForFutureUse : UINT := 10#15321;

We basically cover all the different use cases for the type of diagnosis code. Notice that the test fixture is a code that according to the ETG1020-standard is “reserved for future use”, while our result should be “unspecified”. As we said in the previous post, we should handle all reserved/unknown as “Unspecified”, and thus this is what the output of our function block should be. As we now have three additional test fixtures and results, we also need to make sure to run them in the test-body:

// @TEST-RUN ManufacturerSpecific
fbDiagnosticMessageDiagnosticCodeParser(anDiagnosticCodeBuffer := canDiagnosticCodeBuffer_ManufacturerSpecific,
                                        stDiagnosticCode => stDiagnosticCode);
// @TEST-ASSERT ManufacturerSpecific
bSuccess_fbDiagnosticMessageDiagnosticCodeParser_ManufacturerSpecific := 
    (stDiagnosticCode.eDiagnosticCodeType = ceDiagnosticCodeType_ManufacturerSpecific) AND
    (stDiagnosticCode.nCode = cnDiagnosticCode_ManufacturerSpecific);
// @TEST-RUN ProfileSpecific
fbDiagnosticMessageDiagnosticCodeParser(anDiagnosticCodeBuffer := canDiagnosticCodeBuffer_ProfileSpecific,
                                        stDiagnosticCode => stDiagnosticCode);
// @TEST-ASSERT ProfileSpecific
bSuccess_fbDiagnosticMessageDiagnosticCodeParser_ProfileSpecific := 
    (stDiagnosticCode.eDiagnosticCodeType = ceDiagnosticCodeType_ProfileSpecific) AND
    (stDiagnosticCode.nCode = cnDiagnosticCode_ProfileSpecific);
// @TEST-RUN ReservedForFutureUse
fbDiagnosticMessageDiagnosticCodeParser(anDiagnosticCodeBuffer := canDiagnosticCodeBuffer_ReservedForFutureUse,
                                        stDiagnosticCode => stDiagnosticCode);
// @TEST-ASSERT ReservedForFutureUse
bSuccess_fbDiagnosticMessageDiagnosticCodeParser_ReservedForFutureUse := 
    (stDiagnosticCode.eDiagnosticCodeType = ceDiagnosticCodeType_ReservedForFutureUse) AND
    (stDiagnosticCode.nCode = cnDiagnosticCode_ReservedForFutureUse);

As can be seen, the same function block instance (fbDiagnosticMessageDiagnosticCodeParser) is being run with different inputs (constant arrays), being the raw byte arrays. Every time we run the function block, we assert that the output (stDiagnosticCode) is equal to our expected @TEST-RESULT. Every test-runs result is stored in its own boolean. The final thing we need to do is to make sure that the bSuccess-boolean that is the output of the complete test-FB needs to be set according to the results for these tests, i.e.:

bSuccess := bSuccess_fbDiagnosticMessageDiagnosticCodeParser_EmergencyErrorCode AND
            bSuccess_fbDiagnosticMessageDiagnosticCodeParser_ManufacturerSpecific AND
            bSuccess_fbDiagnosticMessageDiagnosticCodeParser_ProfileSpecific AND

FB_DiagnosticMessageFlagsParser_Test Link to heading

The next function block that we want to write tests for is the one that parses the different flags in the event message. The tests will follow the same layout as for the previous function block, where we:

  • Instantiate the function block under test
  • Declare booleans to store the test-results
  • Declare test-fixtures for our tests
  • Declare the test-results for the test-fixtures

Again, a remainder from the previous post, the layout of the two bytes looks like this:

Bit Description
Bit 0-3 0: Info message, 1: Warning message, 2: Error message, 3-15: Reserved for future use
Bit 4 Time stamp is a local time stamp
Bit 5-7 Reserved for future use
Bit 8-15 Number of parameters in this diagnosis message

A couple of good tests would obviously be to try every code type (info, warning, error) and with some different combinations of timestamp and amount of parameters.

Flags layout

The complete function block header including the test fixtures and test results is:

FUNCTION_BLOCK FB_DiagnosticMessageFlagsParser_Test
    bSuccess : BOOL;
    fbDiagnosticMessageFlagsParser : FB_DiagnosticMessageFlagsParser;
    stFlags : ST_FLAGS;
    bSuccess_fbDiagnosticMessageFlagsParser_InfoMessage : BOOL;
    bSuccess_fbDiagnosticMessageFlagsParser_WarningMessage : BOOL;
    bSuccess_fbDiagnosticMessageFlagsParser_ErrorMessage : BOOL;
    bSuccess_fbDiagnosticMessageFlagsParser_ReservedForFutureUseMessage : BOOL;
    // @TEST-FIXTURE InfoMessage
    cnFlagsBufferByte1_InfoMessage : BYTE := 2#0000_0000; // Info message and global time stamp
    cnFlagsBufferByte2_InfoMessage : BYTE := 2#0000_0000; // Zero parameters in the diagnosis message
    canFlagsBuffer_InfoMessage : ARRAY[1..2] OF BYTE := [cnFlagsBufferByte1_InfoMessage, 
    // @TEST-RESULT InfoMessage
    ceFlags_DiagnosisTypeInfoMessage : E_DIAGNOSISTYPE := E_DIAGNOSISTYPE.InfoMessage;
    ceFlags_TimeStampTypeGlobal : E_TIMESTAMPTYPE := E_TIMESTAMPTYPE.Global;
    cnFlags_NumberOfParametersInDiagnosisMessageZero : USINT := 0;
    // @TEST-FIXTURE WarningMessage
    cnFlagsBufferByte1_WarningMessage : BYTE := 2#0001_0001; // Warning message and local time stamp
    cnFlagsBufferByte2_WarningMessage : BYTE := 2#0000_0010; // Two parameters in the diagnosis message
    canFlagsBuffer_WarningMessage : ARRAY[1..2] OF BYTE := [cnFlagsBufferByte1_WarningMessage, 
    // @TEST-RESULT WarningMessage
    ceFlags_DiagnosisTypeWarningMessage : E_DIAGNOSISTYPE := E_DIAGNOSISTYPE.WarningMessage;
    ceFlags_TimeStampTypeLocal : E_TIMESTAMPTYPE := E_TIMESTAMPTYPE.Local;
    cnFlags_NumberOfParametersInDiagnosisMessageTwo : USINT := 2;
    // @TEST-FIXTURE ErrorMessage
    cnFlagsBufferByte1_ErrorMessage : BYTE := 2#0001_0010; // Error message and local time stamp
    cnFlagsBufferByte2_ErrorMessage : BYTE := 2#0000_0100; // Four parameters in the diagnosis message
    canFlagsBuffer_ErrorMessage : ARRAY[1..2] OF BYTE := [cnFlagsBufferByte1_ErrorMessage, 
    // @TEST-RESULT ErrorMessage
    ceFlags_DiagnosisTypeErrorMessage : E_DIAGNOSISTYPE := E_DIAGNOSISTYPE.ErrorMessage;
    cnFlags_NumberOfParametersInDiagnosisMessageFour : USINT := 4;
    // @TEST-FIXTURE ReservedForFutureUseMessage
    cnFlagsBufferByte1_ReservedForFutureUseMessage : BYTE := 2#0001_0011; // ReservedForFutureUse message and local time stamp
    cnFlagsBufferByte2_ReservedForFutureUseMessage : BYTE := 2#0010_0001; // 33 parameters in the diagnosis message
    canFlagsBuffer_ReservedForFutureUseMessage : ARRAY[1..2] OF BYTE := [cnFlagsBufferByte1_ReservedForFutureUseMessage, 
    // @TEST-RESULT ReservedForFutureUseMessage
    ceFlags_DiagnosisTypeReservedForFutureUseMessage : E_DIAGNOSISTYPE := E_DIAGNOSISTYPE.Unspecified;
    cnFlags_NumberOfParametersInDiagnosisMessage33 : USINT := 33;

What we have here are four different test fixtures. We differentiate between them by changing the contents of the two bytes defining the flags-parameter. By changing the first four bits of the first byte, we change the diagnosis type (info, warning, error, unspecified). To verify that our code outputs a diagnosis type of unspecified, we need to make sure that the first four bits of the first byte have a value of 4-15 (decimal), which is reserved for future use. This is what is done in the fourth text fixture. Finally we need to call the function block under test with all the test fixtures and assert the result for each and one of them, just like we did for the diagnosis code function block previously.

// @TEST-RUN InfoMessage
fbDiagnosticMessageFlagsParser(anFlagsBuffer := canFlagsBuffer_InfoMessage,
                               stFlags => stFlags);
// @TEST-ASSERT InfoMessage
bSuccess_fbDiagnosticMessageFlagsParser_InfoMessage :=
    (stFlags.eDiagnostisType = ceFlags_DiagnosisTypeInfoMessage) AND
    (stFlags.eTimeStampType = ceFlags_TimeStampTypeGlobal) AND
    (stFlags.nNumberOfParametersInDiagnosisMessage = cnFlags_NumberOfParametersInDiagnosisMessageZero);
// @TEST-RUN WarningMessage
fbDiagnosticMessageFlagsParser(anFlagsBuffer := canFlagsBuffer_WarningMessage,
                               stFlags => stFlags);
// @TEST-ASSERT WarningMessage
bSuccess_fbDiagnosticMessageFlagsParser_WarningMessage :=
    (stFlags.eDiagnostisType = ceFlags_DiagnosisTypeWarningMessage) AND
    (stFlags.eTimeStampType = ceFlags_TimeStampTypeLocal) AND
    (stFlags.nNumberOfParametersInDiagnosisMessage = cnFlags_NumberOfParametersInDiagnosisMessageTwo);
// @TEST-RUN ErrorMessage
fbDiagnosticMessageFlagsParser(anFlagsBuffer := canFlagsBuffer_ErrorMessage,
                               stFlags => stFlags);
// @TEST-ASSERT ErrorMessage
bSuccess_fbDiagnosticMessageFlagsParser_ErrorMessage :=
    (stFlags.eDiagnostisType = ceFlags_DiagnosisTypeErrorMessage) AND
    (stFlags.eTimeStampType = ceFlags_TimeStampTypeLocal) AND
    (stFlags.nNumberOfParametersInDiagnosisMessage = cnFlags_NumberOfParametersInDiagnosisMessageFour);
// @TEST-RUN ReservedForFutureUseMessage
fbDiagnosticMessageFlagsParser(anFlagsBuffer := canFlagsBuffer_ReservedForFutureUseMessage,
                               stFlags => stFlags);
// @TEST-ASSERT ReservedForFutureUseMessage
bSuccess_fbDiagnosticMessageFlagsParser_ReservedForFutureUseMessage :=
    (stFlags.eDiagnostisType = ceFlags_DiagnosisTypeReservedForFutureUseMessage) AND
    (stFlags.eTimeStampType = ceFlags_TimeStampTypeLocal) AND
    (stFlags.nNumberOfParametersInDiagnosisMessage = cnFlags_NumberOfParametersInDiagnosisMessage33);
bSuccess := bSuccess_fbDiagnosticMessageFlagsParser_InfoMessage AND
            bSuccess_fbDiagnosticMessageFlagsParser_WarningMessage AND
            bSuccess_fbDiagnosticMessageFlagsParser_ErrorMessage AND

In the next post we’ll continue to create the unit tests for the remaining function blocks that handle the event logs on IO-Link.