In the previous post we defined the general layout of our unit tests, and also did the implementation of the tests for two of the five function block that we’re going to use to verify the functionality of parsing IO-Link events. What we’ve got left is to create test cases for the parsing of the text identity and the timestamp of the diagnostic event. Then we also want to have a few tests that closes the loop and verifies the parsing of a complete diagnosis history message.
FB_DiagnosticMessageTextIdentityParser_Test
The only input for the text identity are two bytes that together make up an unsigned integer (0-65535), which is the result (output) of this parser. It’s enough to make three test cases; one for low/medium/max. This means we’ll need three booleans to keep track of whether our unit tests have failed or not. We accomplish to test the three values by changing the two bytes that make up the unsigned integer. The header of the function block unit test:
FUNCTION_BLOCK FB_DiagnosticMessageTextIdentityParser_Test VAR_OUTPUT bSuccess : BOOL; END_VAR VAR fbDiagnosticMessageTextIdentityParser : FB_DiagnosticMessageTextIdentityParser; nTextIdentity : UINT; bSuccess_fbDiagnosticMessageTextIdentityParser_IdentityLow : BOOL; bSuccess_fbDiagnosticMessageTextIdentityParser_IdentityHigh : BOOL; bSuccess_fbDiagnosticMessageTextIdentityParser_IdentityMed : BOOL; END_VAR VAR CONSTANT // @TEST-FIXTURE TextIdentity#Low cnTextIdentityBufferByte1_IdentityLow : BYTE := 16#00; // 0 = no text identity cnTextIdentityBufferByte2_IdentityLow : BYTE := 16#00; canTextIdentityBuffer_IdentityLow : ARRAY[1..2] OF BYTE := [cnTextIdentityBufferByte1_IdentityLow, cnTextIdentityBufferByte2_IdentityLow]; // @TEST-RESULT TextIdentity#Low cnTextIdentity_IdentityLow : UINT := 0; // @TEST-FIXTURE TextIdentity#High cnTextIdentityBufferByte1_IdentityHigh : BYTE := 16#FF; // 0xFFFF = 65535 cnTextIdentityBufferByte2_IdentityHigh : BYTE := 16#FF; canTextIdentityBuffer_IdentityHigh : ARRAY[1..2] OF BYTE := [cnTextIdentityBufferByte1_IdentityHigh, cnTextIdentityBufferByte2_IdentityHigh]; // @TEST-RESULT TextIdentity#High cnTextIdentity_IdentityHigh : UINT := 65535; // @TEST-FIXTURE TextIdentity#Med cnTextIdentityBufferByte1_IdentityMed : BYTE := 16#C4; // 0x86C4 = 34500 cnTextIdentityBufferByte2_IdentityMed : BYTE := 16#86; canTextIdentityBuffer_IdentityMed : ARRAY[1..2] OF BYTE := [cnTextIdentityBufferByte1_IdentityMed, cnTextIdentityBufferByte2_IdentityMed]; // @TEST-RESULT TextIdentity#Med cnTextIdentity_IdentityMed : UINT := 34500; END_VAR
We instantiate the function block that we are going to test (fbDiagnosticMessageTextIdentityParser), store the result (nTextIdentity) and prepare three test-results (bSuccess_Low/Med/High). Then we prepare the three test-fixtures and results for all three tests.
And as usual our body runs the tests and stores the results:
// @TEST-RUN TextIdentity#Low fbDiagnosticMessageTextIdentityParser(anTextIdentityBuffer := canTextIdentityBuffer_IdentityLow, nTextIdentity => nTextIdentity); // @TEST-ASSERT TextIdentity#Low bSuccess_fbDiagnosticMessageTextIdentityParser_IdentityLow := (nTextIdentity = cnTextIdentity_IdentityLow); // @TEST-RUN TextIdentity#High fbDiagnosticMessageTextIdentityParser(anTextIdentityBuffer := canTextIdentityBuffer_IdentityHigh, nTextIdentity => nTextIdentity); // @TEST-ASSERT TextIdentity#High bSuccess_fbDiagnosticMessageTextIdentityParser_IdentityHigh := (nTextIdentity = cnTextIdentity_IdentityHigh); // @TEST-RUN TextIdentity#Med fbDiagnosticMessageTextIdentityParser(anTextIdentityBuffer := canTextIdentityBuffer_IdentityMed, nTextIdentity => nTextIdentity); // @TEST-ASSERT TextIdentity#Med bSuccess_fbDiagnosticMessageTextIdentityParser_IdentityMed := (nTextIdentity = cnTextIdentity_IdentityMed); bSuccess := bSuccess_fbDiagnosticMessageTextIdentityParser_IdentityLow AND bSuccess_fbDiagnosticMessageTextIdentityParser_IdentityHigh AND bSuccess_fbDiagnosticMessageTextIdentityParser_IdentityMed;
FB_DiagnosticMessageTimeStampParser_Test
The eight bytes that make up the timestamp can be either the distributed clock (DC) from EtherCAT, or a local clock in the device itself. In the global case we want to parse the DC-time, while in the local case we just want to take the DC clock from the current task time (Again, the local clock could be extracted from the EtherCAT-slave, but for the sake of simplicity we’ll use the task DC-clock). Because the local/global-flag is read from the “Flags”-FB, this information needs to be provided into the timestamp-FB, and is therefore an input to the FB. What this means is that if the timestamp is local, the eight bytes don’t matter as we’ll get the time from the task. For the timestamp-FB it’s enough with two test cases, one testing it with a local timestamp and the other with a global timestamp. The local timestamp unit test result has to be created in runtime.
FUNCTION_BLOCK FB_DiagnosticMessageTimeStampParser_Test VAR_OUTPUT bSuccess : BOOL; END_VAR VAR fbDiagnosticMessageTimeStampParser : FB_DiagnosticMessageTimeStampParser; sTimeStamp : STRING(29); bSuccess_fbDiagnosticMessageTextIdentityParser_LocalTimeStamp : BOOL; bSuccess_fbDiagnosticMessageTextIdentityParser_GlobalTimeStamp : BOOL; END_VAR VAR CONSTANT // @TEST-FIXTURE local time stamp canTimeStampBuffer_LocalTimeStamp : ARRAY[1..8] OF BYTE := [8(16#00)]; // @TEST-FIXTURE global time stamp cnTimeStampBufferByte1_GlobalTimeStamp : BYTE := 16#C0; // 0x07C76560A71025C0 = '2017-10-05-14:15:44.425035200' cnTimeStampBufferByte2_GlobalTimeStamp : BYTE := 16#25; cnTimeStampBufferByte3_GlobalTimeStamp : BYTE := 16#10; cnTimeStampBufferByte4_GlobalTimeStamp : BYTE := 16#A7; cnTimeStampBufferByte5_GlobalTimeStamp : BYTE := 16#60; cnTimeStampBufferByte6_GlobalTimeStamp : BYTE := 16#65; cnTimeStampBufferByte7_GlobalTimeStamp : BYTE := 16#C7; cnTimeStampBufferByte8_GlobalTimeStamp : BYTE := 16#07; canTimeStampBuffer_GlobalTimeStamp : ARRAY[1..8] OF BYTE := [cnTimeStampBufferByte1_GlobalTimeStamp, cnTimeStampBufferByte2_GlobalTimeStamp, cnTimeStampBufferByte3_GlobalTimeStamp, cnTimeStampBufferByte4_GlobalTimeStamp, cnTimeStampBufferByte5_GlobalTimeStamp, cnTimeStampBufferByte6_GlobalTimeStamp, cnTimeStampBufferByte7_GlobalTimeStamp, cnTimeStampBufferByte8_GlobalTimeStamp]; // @TEST-RESULT global time stamp csTimeStamp_GlobalTimeStamp : STRING(29) := '2017-10-05-14:15:44.425035200'; // T_DCTime64 = 16#07C76560A71025C0 END_VAR
For the local timestamp case, we can see that we setup the test-fixture for the eight bytes to zeros, as this data is not necessary for the local timestamp case. For the global timestamp test-fixture, we created eight bytes of data representing the date/time “2017-10-05-14:15:44.425035200”. As our timestamp-FB returns a string, this is exactly the string that we expect to get as a test-result. You might be asking yourself “how on earth is it possible to know that 0x07C76560A71025C0 equals 2017-10-05-14:15:44.425035200”? What I did was to create a little program that just printed the current actual DC-time by using F_GetActualDCTime64 in combination with DCTIME64_TO_STRING. Because the T_DCTIME64-type that is returned from F_GetActualDcTime64() is an alias for a primitive type, it’s easy to convert it into a byte-array.
The body of the test-FB needs to run these two tests:
// @TEST-RUN local time stamp fbDiagnosticMessageTimeStampParser(anTimeStampBuffer := canTimeStampBuffer_LocalTimeStamp, bIsLocalTime := TRUE, sTimeStamp => sTimeStamp); // @TEST-ASSERT local time stamp bSuccess_fbDiagnosticMessageTextIdentityParser_LocalTimeStamp := ( sTimeStamp = DCTIME64_TO_STRING(in := F_GetCurDcTaskTime64())); // @TEST-RUN global time stamp fbDiagnosticMessageTimeStampParser(anTimeStampBuffer := canTimeStampBuffer_GlobalTimeStamp, bIsLocalTime := FALSE, sTimeStamp => sTimeStamp); // @TEST-ASSERT global time stamp bSuccess_fbDiagnosticMessageTextIdentityParser_GlobalTimeStamp := (sTimeStamp = csTimeStamp_GlobalTimeStamp); bSuccess := bSuccess_fbDiagnosticMessageTextIdentityParser_LocalTimeStamp AND bSuccess_fbDiagnosticMessageTextIdentityParser_GlobalTimeStamp;
Note that the assertion of the local time stamp is based on getting the current DC-task time by utilizing the F_GetCurDcTaskTime64(), thus we’re making sure that if the diagnosis message tells us that the timestamp is a local clock, we check that our FB returns this.
FB_DiagnosticMessageParser_Test
The final test-FB that we need is the one that ties the bag together and uses all the other four. The FB_DiagnosticMessageParser function block will be the one where we send in all the bytes that we receive from the IO-Link master, and that will output the struct that we can present to the operator or send further up in the chain. One could argue that because we already have unit tests for the other four function blocks, we don’t need to have unit tests for this one. By having unit tests for this “umbrella” function block, we add an additional level of confidence that our code is working properly. Additionally, we can also make sure that combinations of different diagnosis messages are parsed correctly.
To have maximum variation we want to try to vary all parameters as much as possible. I’m not going to write down all variations here, but will only show one example of a test case that we want to run and assert. I’ll try to explain it as detailed as possible, thus it should thus be easy for you to add any test cases that you find necessary. As usual, header first:
FUNCTION_BLOCK FB_DiagnosticMessageParser_Test VAR_OUTPUT bSuccess : BOOL; END_VAR VAR fbDiagnosticMessageParser : FB_DiagnosticMessageParser; stDiagnosticMessage : ST_DIAGNOSTICMESSAGE; fbDiagnosticMessageComparator : FB_DiagnosticMessageComparator; bSuccess_fbDiagnosticMessageParser_ManufacturerSpecificMessage : BOOL; END_VAR
The function block “FB_DiagnosticMessageParser” is a very simple FB that compares every data element of the struct “ST_DIAGNOSTICMESSAGE”, which we’ll later use when we’re doing the assertion. The boolean “bSuccess_fbDiagnosticMessageParser_ManufacturerSpecificMessage” holds the result of the unit test that we’re going to run. Here we can add any additional test-result booleans for any additional tests that we want to run (which is something we would like to do).
VAR CONSTANT // @TEST-FIXTURE ManufacturerSpecificMessage cnDiagnosticBufferByte1_ManufacturerSpecificMessage : BYTE := 16#90; // 0xE290 = Manufacturer Specific cnDiagnosticBufferByte2_ManufacturerSpecificMessage : BYTE := 16#E2; cnDiagnosticBufferByte3_ManufacturerSpecificMessage : BYTE := 16#30; // 0x0000 = Code 0 cnDiagnosticBufferByte4_ManufacturerSpecificMessage : BYTE := 16#75; cnDiagnosticBufferByte5_ManufacturerSpecificMessage : BYTE := 2#0000_0000; // Global time stamp & info message cnDiagnosticBufferByte6_ManufacturerSpecificMessage : BYTE := 16#02; // Number of parameters = 2 cnDiagnosticBufferByte7_ManufacturerSpecificMessage : BYTE := 16#A8; // 0x61A8, Text id as reference to ESI file = 10#25000 cnDiagnosticBufferByte8_ManufacturerSpecificMessage : BYTE := 16#61; cnDiagnosticBufferByte9_ManufacturerSpecificMessage : BYTE := 16#C8; // Timestamp from DC clock, 16#07C8D11492616FC8 = '2017-10-10-05:20:39.893037000' cnDiagnosticBufferByte10_ManufacturerSpecificMessage : BYTE := 16#6F; cnDiagnosticBufferByte11_ManufacturerSpecificMessage : BYTE := 16#61; cnDiagnosticBufferByte12_ManufacturerSpecificMessage : BYTE := 16#92; cnDiagnosticBufferByte13_ManufacturerSpecificMessage : BYTE := 16#14; cnDiagnosticBufferByte14_ManufacturerSpecificMessage : BYTE := 16#D1; cnDiagnosticBufferByte15_ManufacturerSpecificMessage : BYTE := 16#C8; cnDiagnosticBufferByte16_ManufacturerSpecificMessage : BYTE := 16#07; canDiagnosticBuffer_ManufacturerSpecificMessage : ARRAY[1..28] OF BYTE := [ cnDiagnosticBufferByte1_ManufacturerSpecificMessage, cnDiagnosticBufferByte2_ManufacturerSpecificMessage, cnDiagnosticBufferByte3_ManufacturerSpecificMessage, cnDiagnosticBufferByte4_ManufacturerSpecificMessage, cnDiagnosticBufferByte5_ManufacturerSpecificMessage, cnDiagnosticBufferByte6_ManufacturerSpecificMessage, cnDiagnosticBufferByte7_ManufacturerSpecificMessage, cnDiagnosticBufferByte8_ManufacturerSpecificMessage, cnDiagnosticBufferByte9_ManufacturerSpecificMessage, cnDiagnosticBufferByte10_ManufacturerSpecificMessage, cnDiagnosticBufferByte11_ManufacturerSpecificMessage, cnDiagnosticBufferByte12_ManufacturerSpecificMessage, cnDiagnosticBufferByte13_ManufacturerSpecificMessage, cnDiagnosticBufferByte14_ManufacturerSpecificMessage, cnDiagnosticBufferByte15_ManufacturerSpecificMessage, cnDiagnosticBufferByte16_ManufacturerSpecificMessage]; // @TEST-RESULT ManufacturerSpecificMessage cstDiagnosticMessage_ManufacturerSpecificMessage : ST_DIAGNOSTICMESSAGE := (stDiagnosticCode := (eDiagnosticCodeType := E_DIAGNOSTICCODETYPE.ManufacturerSpecific, nCode := 30000), stFlags := (eDiagnostisType := E_DIAGNOSISTYPE.InfoMessage, eTimeStampType := E_TIMESTAMPTYPE.Global, nNumberOfParametersInDiagnosisMessage := 2), nTextIdentityReferenceToESIFile := 25000, sTimeStamp := '2017-10-10-05:20:39.893037000'); END_VAR
As usual, we’ll declare the test-fixture and the expected test-results for that fixture. I’ve commented all the bytes so that it’s obvious what information is being stored in every byte. Note that the test-result is a structured representation of all the diagnostic history message bytes. These 16 bytes are the sum of all the other bytes used for the other four function block parsers.
That’s our tests
And that’s it. We’re finished with the unit tests. Notice that we still have not written a single line of code for the implementing part. We’ve defined the inputs/outputs and the accompanying data structures for our function blocks. We’ve also created all the test cases. All of the code you’ve written so far is excellent documentation for any other developer that would try to understand the implementing code in the future. Not only that, all your test cases also form the acceptance criteria for the implementation code. You’ve basically said “I require my code to pass these tests, and these tests must pass for my code to do what I want it to do”. Note that all the tests above can be executed at anytime. Done any change to your code? Just re-run the tests and make sure you haven’t broken anything. Fantastic, isn’t it?
In part five of this series we are going to start with the actual implementation of the required functionality, in which our goal is to eventually make the tests pass. See you next week!
Algorithm icon by Freepik from www.flaticon.com
Hi Jakob,
Great job, but I think there are some little misprints here. First, it should be “nTextIdentity => nTextIdentity” instead of “nTextIdentity => nTextIdentity” and “sTimeStamp => sTimeStamp” instead of “sTimeStamp => sTimeStamp”.
Secondly, I guess you were talking about the FB_DiagnosticMessageComparator in the sentence:
“The function block “FB_DiagnosticMessageParser” is a very simple FB that compares every data element of the struct “ST_DIAGNOSTICMESSAGE”, which we’ll later use when we’re doing the assertion.”
Thus it should be:
“The function block “FB_ DiagnosticMessageComparator” is a very simple FB that compares every data element of the struct “ST_DIAGNOSTICMESSAGE”, which we’ll later use when we’re doing the assertion.”
And finally, I don’t see the body of the FB_DiagnosticMessageParser_Test in your post. Here is my implementation of this FB without using fbDiagnosticMessageComparator, if I understand the idea correctly:
// @TEST-RUN
fbDiagnosticMessageParser(anDiagnosticMessageBuffer:=canDiagnosticBuffer_ManufacturerSpecificMessage, stDiagnosticMessage=>stDiagnosticMessage);
// @TEST-ASSERT
bSuccess_fbDiagnosticMessageParser_ManufacturerSpecificMessage:= ((stDiagnosticMessage.nTextIdentityReferenceToESIFile=cstDiagnosticMessage_ManufacturerSpecificMessage.nTextIdentityReferenceToESIFile) AND
(stDiagnosticMessage.stDiagnosticCode.eDiagnosticCodeType=cstDiagnosticMessage_ManufacturerSpecificMessage.stDiagnosticCode.eDiagnosticCodeType) AND
(stDiagnosticMessage.stDiagnosticCode.nCode=cstDiagnosticMessage_ManufacturerSpecificMessage.stDiagnosticCode.nCode) AND
(stDiagnosticMessage.stFlags.eDiagnostisType=cstDiagnosticMessage_ManufacturerSpecificMessage.stFlags.eDiagnostisType) AND
(stDiagnosticMessage.stFlags.eTimeStampType=cstDiagnosticMessage_ManufacturerSpecificMessage.stFlags.eTimeStampType) AND
(stDiagnosticMessage.stFlags.nNumberOfParametersInDiagnosisMessage=cstDiagnosticMessage_ManufacturerSpecificMessage.stFlags.nNumberOfParametersInDiagnosisMessage) AND
(stDiagnosticMessage.sTimeStamp=cstDiagnosticMessage_ManufacturerSpecificMessage.sTimeStamp)
);
bSuccess := bSuccess_fbDiagnosticMessageParser_ManufacturerSpecificMessage;