Assertion: All non-trivial algorithmic code should be contained in a library. Rationale: This allows reuse in future projects, thorough testing in a controlled context, and fine-grained control of revisions via version numbering.
Writing libraries requires that the code can be executed somehow as it is developed, as code rarely works first time. And code that lives in a library carries with it a not unreasonable expectation that it works correctly. The best way of achieving this is with unit tests running in some sort of framework.
To this end, I wrote a unit test library a few years ago (https://github.com/RedRockControls/SimpleUnitTestLibrary), and have used it since on many libraries and projects. However, I never found a satisfactory way of making units tests easily portable between the library and the project in which the library was to be used, and using the PLC visualization to show test results introduced too many dependencies. Hence the new library (https://github.com/RedRockControls/tcl_TwinCAT_UnitTestLibrary)
The aim of making unit tests easily portable is to allow the tests to be written and used in the library project during development, and in the client code when the library is to be actually used in a project.
Running the test in the library project means you are not continually creating the library and switching to another project to run the tests. And if the tests can run automatically on an online change to the code, then fast feedback is possible. This is especially useful when practicing Test Driven Development (TDD).
Running the same tests in the client project means that the library code is tested in the actual context in which it will be utilised – i.e the same hardware platform and libraries (ARM processors do not behave identically to x86 processors!)
Using The Library
Creating a unit test
A unit test can be any function block that extends the function block T_UnitTestBase. The test is defined as follows:
The Init method is overridden with initialisation code to be executed before the test code is executed
The RunTest method is overridden with the test code that executes the test and writes to the two output variables – TestFailed and TestCompleted
So a test can be run just by executing the Init method to perform any initialisation required, then executing RunTest until TestCompleted is TRUE. We can then inspect the TestFailed output to determine the result of the test.
However, what we really need is a collection of unit tests that can be executed automatically on an online change to the code. For this, we need a Test Suite
An example of a test suite is shown below:
A test suite is function block that holds a collection of unit tests and a test runner. The test runner instance is passed into the declaration of each test. This is required so that the unit test instance can be added to a list of tests maintained by the test runner.
The test runner is executed in the main body of the Test Suite. This executes each unit test sequentially when an online change to the code is detected. The name of the test suite is passed to the test runner so that messages posted to the error window of Visual Studio can be identified accordingly. An optional boolean parameter ‘Verbose’ can also be passed in. If true, then test results for each test are posted to the message window as each test completes.
Results with Verbose TRUE:
Results with Verbose FALSE:
So now we have a function block defined in a library that contains unit tests and a means of running them. This means we can run the tests from the client project by declaring an instance of the test suite and executing it.
Even better is that if the library is extended to include new functionality, and unit tests are added to the test suite to test this new functionality, then these tests will propagate to the tests in the client project when the library reference is updated.
Assertion functions are available to test the correct operation of the code under test. If the assertion fails, it posts an error message to the Error List and sets its Failed output. This can be used to set the TestFailed output of the RunTest method.
The following assertions are defined in the library:
AssertTrue – asserts that the boolean input parameter (Condition) is true
AssertEquals – asserts that two input parameters of type ANY (Expected and Actual) are identical in type, size and content.
AssertNearlyEquals – asserts that the absolute difference between two input parameters of type LREAL (Expected and Actual) is less than the value of a third input parameter (Delta). This is to make the test insensitive to rounding errors in floating point calculations.
If a unit test is added to a test suite, the code must be downloaded to the runtime – no online change is possible. This appears to be due to a bug in the way nested function blocks are initialised. If an online change is attempted, an error is raised :
Error Function ‘FB_init’ requires exactly ‘3’ inputs.