Introduction user guide

Before reading this guide, it’s highly recommended to read unit testing concepts, in order to have a basic understanding of the concepts of unit testing and TcUnit.

The TcUnit framework gives you the possibility to easily write unit tests for your TwinCAT3 software, and having the results reported in a human-readable format for review. All unit test code is written in the same program/library as the rest of your code, but because it is only used in a separate test-program, it does not affect the production code/executables. With unit test-code provided with the rest of the code, you can see these additions as living documentation of the code. For a more thorough/detailed example please see the programming example.

The purpose of this user guide is to be a short tutorial where we will go through the different steps to that are necessary to use TcUnit, which are:

  1. Download and install the TcUnit framework on your engineering PC
  2. Reference the library in your project
  3. Create test function blocks and run the tests

1. Download & install

The framework can either be downloaded as a precompiled library, or you can download the source code and compile the library yourself.

Install from library file

If you’ve downloaded the library, you should have a file called TcUnit.compiled-library in your computer. Start your TwinCAT XAE (Visual Studio). In the menu of visual studio select PLC and then Library Repository… This will display the following window:

Library repository window in TwinCAT XAE

Click on Install…, locate the TcUnit.compiled-library file and double-click on it. Now it will install to your TwinCAT-folder, more specifically C:\TwinCAT\3.1\Components\Plc\Managed Libraries\www.tcunit.org\TcUnit\.

Install from source

If you want to install it from source, make sure that you have a TwinCAT XAE installed. Next do a GIT-clone on the repository. Open the folder where you cloned the repo, and open the solution by double-clicking on the TcUnit.sln file in the root of the folder, which will open the project in your TwinCAT XAE environment. In the solution explorer, locate the node TcUnit Project and right-click on it, select Save as library and install…

Save as library and install…

Select the desktop of your computer for where to save the file, change the file format in the drop-down menu from Library files (*.library) to Compiled library files (*.compiled-library) and click on Save. This will install the library on your computer. Once the library is installed, the file that you saved on the desktop can be removed.

2. Reference the library in project

In order to use TcUnit you need to add a reference to the library in your project. Open your TwinCAT project, and right-click on the References under the PLC-project and click on Add library…

Add library reference

Next go to the TcUnit-group, select TcUnit and click OK.

3. Create test function blocks (FBs) and run them

For every function block (or free function) that you have defined we want to create a test function block (FB), which has the responsibility to:

  • instantiate the FB under test
  • define the inputs
  • define the expected outputs (result)
  • call the TcUnit-assert methods to compare the expected output to the actual output for every test

It’s entirely up to the user how to to organize the different tests, so what follows is a best practice/suggestion. On the same level as the POUs folder, create a folder called Test. It’s in this folder that we will create all our test FBs as well as the program that will run the TcUnit framework.

Test function blocks

In this example we have a total of five FBs. For every FB we have created a test-FB, i.e. for FB_DiagnosticMessageDiagnosticCodeParser we have FB_DiagnosticMessageDiagnosticCodeParser_Test. Note that the framework in no way enforces to use any standard for the naming, this is entirely up to the user. For various reasons you might find it not even be possible to add the test-FBs in the same solution (for instance, if this is your main executable), and in this case just put the tests in a separate solution and include the main solution in the test solution as a library. Generally it’s better to structure the code in various library projects each responsible for a certain set of requirements/functionality. It’s important to see the tests as an important part of your code.

General test architecture

The general structure here is that PRG_TEST is the program in where the test-FBs (test suites) are instantiated. Each test suite is responsible of testing one FB or function, and can have one or more tests to do so.

Let’s assume we want to create the simplest possible FB that takes two unsigned integers and sums them. We can create the header for the FB, but the actual implementation can (and should) wait after we’ve done the unit tests.

FUNCTION_BLOCK FB_Sum
VAR_INPUT
    one : UINT;
    two : UINT;
END_VAR
VAR_OUTPUT
    result : UINT;
END_VAR

Now let’s create the test-FB for this. This FB needs to:

  • extend TcUnit.FB_TestSuite
  • implement the interface TcUnit.I_RunnableTestSuite
  • have the attribute-pragma {attribute ‘call_after_init’}
  • create an instance of TcUnit.FB_Assert to do our assertions
  • have the FB_init constructor, only registering the instance of the FB to the test framework with the line SUPER^.RegisterTestSuite(THIS^); in the body of FB_init
{attribute 'call_after_init'}
FUNCTION_BLOCK FB_Sum_Test EXTENDS TcUnit.FB_TestSuite IMPLEMENTS TcUnit.I_RunnableTestSuite
VAR
    Assert : TcUnit.FB_Assert;
END_VAR

To create a FB_init method, simply right click on your test-FB (test suite), select Add->Method, in the name you can select FB_init, then click Open.

Creation of FB_init

The FB_init needs to look like this:

METHOD FB_init : BOOL
VAR_INPUT
    bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start)
    bInCopyCode : BOOL;  // if TRUE, the instance afterwards gets moved into the copy code (online change)
END_VAR

SUPER^.RegisterTestSuite(THIS^);

In this way the testing framework has a handle to your test suites. Next we need to right-click on the test suite FB and select Implement interfaces… Select Structured Text (ST). This will create the method RunTests, which is the method called by TcUnit to execute all the tests. The return value of this method is an enumeration, and it’s necessary to set this to E_TestSuiteRunState.FINISHED when all the tests in the test suite are finished to indicate to TcUnit to stop calling that particular test suite in the next cycle. This gives the flexibility to have tests that span over more than one PLC-cycle.

Now it’s time to create our tests. There are many ways to structure your tests, and there are several guidelines for this as well. What we’ll be doing is to create a method for every test, and name it in such a way that it’s clear what the test does. Remember that the unit tests are part of the documentation of your code, and although you might find the code trivial at this moment, there might be other developers reading your code now (or many years in the future). For them well-named tests are invaluable. We’ll be creating two tests called TwoPlusTwoEqualsFour and ZeroPlusZeroEqualsZero. The TwoPlusTwoEqualsFour will look like this:

METHOD TwoPlusTwoEqualsFour
VAR
    Sum : FB_Sum;
    Result : UINT;
    ExpectedSum : UINT := 4;
END_VAR

TEST('TwoPlusTwoEqualsFour');

Sum(one := 2, two := 2, result => Result);

Assert.AssertEquals(Expected := ExpectedSum,
                    Actual := Result,
                    Message := 'The calculation is not correct');

By calling TEST() we tell TcUnit that everything that follows is a test. In the Assert-FB we have methods to check for all the data types available in IEC61131-3, including the ANY-type. The Message parameter is optional and is used in case the assertion fails, the text is appended to the error output. For ZeroPlusZeroEqualsZero it’s more or less the same code.

METHOD ZeroPlusZeroEqualsZero
VAR
    Sum : FB_Sum;
    Result : UINT;
    ExpectedSum : UINT := 0;
END_VAR

TEST('ZeroPlusZeroEqualsZero');

Sum(one := 0, two := 0, result => Result);

Assert.AssertEquals(Expected := ExpectedSum,
                    Actual := Result,
                    Message := 'The calculation is not correct');

Next we need to update the RunTests()-method to make sure these two tests are being run.

METHOD RunTests : TcUnit.E_TestSuiteRunState

TwoPlusTwoEqualsFour();
ZeroPlusZeroEqualsZero();

RunTests := TcUnit.E_TestSuiteRunState.FINISHED;

Last but not least, we need to have a program PRG_TEST defined in a task that we can run locally on our engineering PC. Note that this program is only created to run the unit-tests, but will never be run on the target PLC. Being part of the library project we only want a convenient way to test all the FBs part of our library, and thus need this program to execute the test suites.

PRG_TEST needs to instantiate all the test suites, and only execute one line of code. In this case we only have one test suite.

PROGRAM PRG_TEST
VAR
    fbSum_Test : FB_Sum_Test; // This is our test suite
END_VAR

TcUnit.RUN();

What we have now is this:

Architecture of unit tests

Activating this solution and running it results in the following result in the visual studio error list:

TcUnit result

There is one test that has failed, and the reason for this is that we have not written the implementation code yet, only the header of the function block FB_Sum. But how come that we have one test succeeding? As we can see, the test TwoPlusTwoEqualsFour failed, which means that the one that succeeded was the other test ZeroPlusZeroEqualsZero. The reason this succeeds is that the default return value for an output-parameter is zero, and thus it means that even if we haven’t written the body of FB_Sum the test will succeed. Let’s finish by implementing the body of FB_Sum.

FUNCTION_BLOCK FB_Sum
VAR_INPUT
    one : UINT;
    two : UINT;
END_VAR
VAR_OUTPUT
    result : UINT;
END_VAR

result := one + two;

Running the tests again we get the expected behavior:

Tests successful

If your output does not seem to be correctly formatted, it might be because Visual Studio is not sorting the output correctly. Press on the Description column until there is an arrow pointing up, such as (see marking in red):

Sort by description

Obviously this is a very simple example and the purpose of this was to show how to use the framework rather to come up with a real-world example. Simple functionality that does not require any state would be better suited to be implemented as a function, or in this case just using the “+” operator. For a real-world example see the programming example.

The source code for this example is available on GitHub.

Last Updated on