Writing tests for FBs with IO variables within

61 Views Asked by At

As the title suggests, I was wondering how one goes about testing FBs that have IO variables declared inside of the function block it-self. I am using TcUnit to write tests for a motion library that I made. Right now the only solution I see is to create the outputs from within the test block and link it to the test FB's input that I can't write to from outside. Another option is to use pointers, but I am really not a fan of that, though it might be the only "proper" way to go about this.

Solution using IO linking

Program:

PROGRAM INTERNAL PRG_TESTS
VAR
    fbTest  : FB_Test;
END_VAR

Test FB:

FUNCTION_BLOCK FB_Test EXTENDS FB_TestSuite
VAR
    CantBeModifiedInput AT %I* : WORD;
    HackToModifyTheInput AT %Q* : WORD;
END_VAR

TestMethod();

Test method:

METHOD TestMethod

TEST('test');
HackToModifyTheInput := 10;

AssertTrue(CantBeModifiedInput = 10, 'Exepected value to be 10.');
TEST_FINISHED();

enter image description here

This however makes it so that only manual tests can be run, this can't be done automatically on a build server - or can it? I am not at the point of automated tests yet, but it is something to consider for the future.

Solution using direct memory access:

METHOD TestMethod
VAR
    testWord    : WORD := 10;
END_VAR

//HackToModifyTheInput := 10;
TEST('test');

MEMCPY(
    destAddr := ADR(CantBeModifiedInput), 
    srcAddr := ADR(testWord), 
    n := SIZEOF(WORD));

AssertTrue(CantBeModifiedInput = 10, 'Exepected value to be 10.');
TEST_FINISHED();

How should I tackle this issue? I really don't want to use inputs and outputs to the fb instead of direct I/O creation from within the block. I feel like the memory manipulation might be the only proper way to be honest.

2

There are 2 best solutions below

0
Roald On BEST ANSWER

From the TcUnit FAQ.

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* or AT %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

2
Jakob On

The more important questions is; what do you want to achieve?

Whether you compile your tests locally on or a build-server doesn't matter. They still need to execute as a TwinCAT task.

It's not entirely clear from your example what it is what you want to achieve. Why would you define some %Q and %I declarations in your test-suite? You don't want to test that TwinCAT I/O linking (through %I and %Q) is working properly. This is (hopefully) already verified by Beckhoff. What you want to verify is your own business logic, and this should not require a full I/O linking to be in place for you to write a test. A test has to be easy to implement and should require in functionality that is easy to write without requiring too much setup.

You are mentioning that you are testing some form of motion function block. If that motion function block is dependent on some external data (like %I), then define that as a dependency (either through constructor or method injection) to your subject under test. In this case you can manipulate the dependencies in either way you want (through for example creation of mocks) instead of trying to use raw pointers to modify those values.