diff --git a/docs/faq.md b/docs/faq.md index 97abeef..6899a54 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -4,6 +4,7 @@ title: F.A.Q. --- ## Frequently asked questions + Here you’ll find the most commonly asked questions and their answers. If you don’t find what you are looking for here, you can look through the: @@ -27,8 +28,8 @@ If you don’t find what you are looking for here, you can look through the: --- - ### 1. How can I run a test across multiple PLC cycles? + This can be accomplished by keeping the function block under test as an instance variable of the test suite rather than the test method. You can download an [example here](https://tcunit.org/temp/TimedTest_1x.zip). In this example, the `FB_ToBeTested` is instantiated under the test suite (`FB_ToBeTested_Test`), and can thus be controlled over multiple cycles. @@ -37,9 +38,10 @@ Then all that’s necessary to do is to set the condition for when the assertion **Required TcUnit version:** 1.0 or later ### 2. How can I disable/ignore a test? + Add `DISABLED_` in front of the test name, for example: -``` +```StructuredText TEST('DISABLED_ThisTestWillBeIgnored'); AssertEquals(Expected := a, @@ -48,9 +50,11 @@ AssertEquals(Expected := a, TEST_FINISHED(); ``` + **Required TcUnit version:** 1.0 or later -### 3. Is there a way to test %I* or %Q* variables? +### 3. Is there a way to test `%I*` or `%Q*` variables? + 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) @@ -59,29 +63,38 @@ In a number of scenarios, TwinCAT won't let you write directly to certain variab 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: -``` + +```StructuredText METHOD PRIVATE TestCommsOkChannelsLow VAR EL1008 : FB_Beckhoff_EL1008; END_VAR ``` + Where the `FB_Beckhoff_EL1008` holds a variable: -``` + +```StructuredText iChannelInput AT %I* : ARRAY[1..8] OF BOOL; ``` + Now you might want to write a value to the first channel of the iChannelInput like: -``` + +```StructuredText TcUnit.WRITE_PROTECTED_BOOL(Ptr := ADR(EL1008.iChannelInput[1]), Value := FALSE); ``` + Whereas afterwards you can make an assertion as usual: -``` + +```StructuredText AssertFalse(Condition := EL1008.ChannelInput[1], Message := 'Channel is not false'); ``` + **Required TcUnit version:** 1.0 or later ### 4. Is there a way to hide TcUnit in my libraries? + You can accomplish this by the [Hide reference](https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_plc_intro/18014402725266443.html&id=) option for referenced libraries. This option lets you hide TcUnit from your other projects. Let’s assume you’ve developed a library `MyLibrary`, which has tests written in TcUnit. @@ -92,17 +105,17 @@ You can find it in the Properties tab: ![Hide reference](img/hide-reference.png) - **Required TcUnit version:** 1.0 or later ### 5. How do I do assertions on the BIT datatype? + I want to do an assertion on two variables both declared with the `BIT`-datatype, but I have noticed that a `AssertEquals_BIT()` does not exist. What do I do? The reason a `AssertEquals_BIT()` does not exist is that TwinCAT does not allow a BIT-datatype as a variable input. If you have data declared with the BIT-type, the easiest way to do an assertion on these is to do a `BIT_TO_BOOL()` conversion and use the `AssertEquals_BOOL()`. -``` +```StructuredText TEST('Testing_of_BIT_Type'); AssertEquals_BOOL(Expected := BIT_TO_BOO(VariableDeclaredAsBit_A), @@ -115,6 +128,7 @@ TEST_FINISHED(); **Required TcUnit version:** 1.0 or later ### 6. When I run more than 100 tests in a single test-suite I get the wrong results, why? + When TcUnit is running it allocates memory in the PLC to store the test results. The maximum number of tests for every test suite has been set to 100, which however is a configuration parameter for TcUnit and can be changed. Parameters for TcUnit (and in fact any library references) are stored in your project, which means that this change will be persistent for your project/library. @@ -124,8 +138,8 @@ To change this max amount, to say for instance 200 tests per test suite, go to t **Required TcUnit version:** 1.0 or later - ### 7. Is it possible to run test suites and/or tests in a sequence? + Yes. By default TcUnit runs all the test suites and tests in parallel, in other words all test suites and tests are run at the same time. Sometimes it is however desirable to run either the test suites or tests (or both) in a sequence, for example if you get exceed overruns while running tests. @@ -134,7 +148,8 @@ Since TcUnit 1.2 it's possible to run test suites in sequence (one after another To execute test suites in a sequence, simply replace `TcUnit.RUN()` with `TcUnit.RUN_IN_SEQUENCE()` in your main body of the test program. This will execute the test suites in the order that they were declared. So for example if we have defined the following test suites and test program: -``` + +```StructuredText PROGRAM PRG_TEST VAR fbDiagnosticMessageDiagnosticCodeParser_Test : FB_DiagnosticMessageDiagnosticCodeParser_Test; @@ -143,14 +158,16 @@ VAR END_VAR ------------------------- TcUnit.RUN_IN_SEQUENCE(); + ``` + This will first execute all tests defined in `fbDiagnosticMessageDiagnosticCodeParser_Test`, once all tests are finished in that function block, TcUnit will execute all tests in `fbDiagnosticMessageFlagsParser_Test`, and when that is done it will execute all tests in `fbDiagnosticMessageParser_Test`. It's also possible to execute individual tests in order by simply replacing `TEST('TestName')` with `TEST_ORDERED('TestName')`. This will execute the tests in the order that the `TEST_ORDERED()` is called for the various tests. `TEST_ORDERED()` returns a boolean to indicate whether the TcUnit framework will run the test, so in order to only execute the code when it's time for that particular test, it makes sense to check if `TEST_ORDERED()` returns true, and only then do the execution of the function blocks and assertions, for example like this: -``` +```StructuredText METHOD PRIVATE TestWithTimestampZeroTimeExpectCurrentTime VAR ... (variable declaration used for the test) @@ -166,6 +183,7 @@ IF TEST_ORDERED('TestWithTimestampZeroTimeExpectCurrentTime') THEN TEST_FINISHED(); END_IF ``` + As usual, the `TEST_FINISHED()` will indicate that this test is finished, and the framework will go to the next test. Note that you don't need to create any state machine for calling the different `TEST_ORDERED()` tests. You can (and must!) call all `TEST_ORDERED()` at the same time. @@ -173,7 +191,6 @@ The framework will make sure to only care about the assertions of the test that This means the following combinations can be used: - - `RUN()` with all tests as `TEST()` – means all tests suites and tests will run in parallel, this is the default behaviour. ![TcUnit run option 1](img/tcunit_run_option1.png) - `RUN_IN_SEQUENCE()` with all tests as `TEST()` – means all test suites will run in sequence, but the tests in every test suite will run in parallel. @@ -197,6 +214,7 @@ For a couple of TwinCAT projects that shows how to run both test suites in a seq **Required TcUnit version:** 1.2 or later ### 8. Why is it taking so long to get the results from TcUnit? + If you have many test suites and/or tests, it can take some time for TcUnit to print all those results. Since version 1.1 of TcUnit, much more data is printed to the ADS-logger as this data is used for the communication with TcUnit-Runner. If you know that you will only run your tests locally and without integration to a CI/CD tool using TcUnit-Runner, you can set the parameter `LogExtendedResults` to `FALSE` (it is default `TRUE`). @@ -207,6 +225,7 @@ To change this parameter, go to the library references and select TcUnit, then g **Required TcUnit version:** 1.1 or later ### 9. Is it possible to have a time delay between the execution of the test suites? + Yes. You can set the parameter `TimeBetweenTestSuitesExecution` to whatever delay you want to have. To change this parameter, go to the library references and select TcUnit, then go to `GVLs` → `GVL_Param_TcUnit` → `TimeBetweenTestSuitesExecution`. @@ -218,9 +237,11 @@ For example, in the below screenshot this is changed to 5 seconds. **Required TcUnit version:** 1.2 or later ### 10. If I call ADSLOGSTR(), my messages don't show up in the correct sequence. Why? + If I call `Tc2_System.ADSLOGSTR()` during execution of a test, my messages don't arrive in the expected order. Let's for example assume this very simple (always failing) test: -``` + +```StructuredText TEST('Test1'); FOR nCounter := 1 TO 5 BY 1 DO Tc2_System.ADSLOGSTR(msgCtrlMask := ADSLOG_MSGTYPE_HINT, @@ -255,9 +276,11 @@ So if we replaced the call to `Tc2_System.ADSLOGSTR()` to `TCUNIT_ADSLOGSTR()` i **Required TcUnit version:** 1.2 or later ### 11. How do I test functions? + It's done almost identical as in the introduction user guide, but simply replace the instance of the function block that you want to test with the call to the function instead. Assume we have a function: -``` + +```StructuredText FUNCTION F_Sum VAR_INPUT one : UINT; @@ -266,8 +289,10 @@ END_VAR F_Sum := one + two; ``` + Then the test would look like following: -``` + +```StructuredText METHOD TwoPlusTwoEqualsFour VAR Result : UINT; @@ -283,11 +308,14 @@ AssertEquals(Expected := ExpectedSum, Message := 'The calculation is not correct'); TEST_FINISHED(); + ``` + **Required TcUnit version:** 1.0 or later ### 12. I have problems running TcUnit on a ARMv7 controller, why? + When running TcUnit with a controller using ARMv7 you can run into issues, such as breakpoints not working. This seems to be an issue with the limited memory of the controllers using an ARMv7 such as the CX8190 and CX9020. Please adjust the [parameters related to memory allocation](#6-when-i-run-more-than-100-tests-in-a-single-test-suite-i-get-the-wrong-results-why). -For more information on a set of working parameters, see [this issue on GitHub](https://github.com/tcunit/TcUnit/issues/148). \ No newline at end of file +For more information on a set of working parameters, see [this issue on GitHub](https://github.com/tcunit/TcUnit/issues/148). diff --git a/docs/img/TcUnit16Of17Failed_2.png b/docs/img/TcUnit16Of17Failed_2.png new file mode 100644 index 0000000..c93cdaf Binary files /dev/null and b/docs/img/TcUnit16Of17Failed_2.png differ diff --git a/docs/img/TcUnitManyFails.png b/docs/img/TcUnitManyFails.png new file mode 100644 index 0000000..32e423a Binary files /dev/null and b/docs/img/TcUnitManyFails.png differ diff --git a/docs/index.md b/docs/index.md index 05b4c52..d7df3e9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,6 +6,8 @@ layout: home list_title: ' ' --- +# TcUnit + ![TcUnit logo](img/tcunit-logo.png) TcUnit is an [xUnit](https://en.wikipedia.org/wiki/XUnit) type of framework specifically done for [Beckhoff’s TwinCAT 3](https://www.beckhoff.com/en-en/products/automation/twincat/) development environment. @@ -17,18 +19,22 @@ Start by reading the [unit testing concepts](unit-testing-concepts.md) and then ![TcUnit introduction](img/tcunit-general.png) ## Easy to use + The framework is easy to use. All that is needed is to download & install the library, and provide a reference to the TcUnit-library in your project, and you can start to write your test code. For a complete set of instructions, start with [the concepts](unit-testing-concepts.md), continue with [the user guide](introduction-user-guide.md) and finish with [the programming example](programming-example.md). ## One library + All functionality is provided by one single library. Add the library to your project and you are ready to go! You can either [download a precompiled](https://github.com/tcunit/TcUnit/releases) (ready to install) version of the library or [download the source code](https://www.github.com/tcunit/tcunit). ## MIT-license + The library and all the source code is licensed according to the MIT-license, which is one of the most relaxed software license terms. The software is completely free and you can use the software in any way you want, be it private or for commercial use as long as you include the MIT license terms with your software. ## Automated test runs + With the additional TcUnit-Runner software, it’s possible to do integrate all your TcUnit tests into a CI/CD software toolchain. -With the aid of automation software such as [Jenkins](https://www.jenkins.io/) or [Azure DevOps](https://azure.microsoft.com/en-us/services/devops/), you can have your tests being run automatically and collect test statistics every time something is changed in your software version control (such as Git or Subversion). \ No newline at end of file +With the aid of automation software such as [Jenkins](https://www.jenkins.io/) or [Azure DevOps](https://azure.microsoft.com/en-us/services/devops/), you can have your tests being run automatically and collect test statistics every time something is changed in your software version control (such as Git or Subversion). diff --git a/docs/introduction-user-guide.md b/docs/introduction-user-guide.md index b08f469..cc885d1 100644 --- a/docs/introduction-user-guide.md +++ b/docs/introduction-user-guide.md @@ -17,9 +17,11 @@ The purpose of this user guide is to be a short tutorial where we will go throug 3. Create test suites and run the tests ## Download & install + The framework can either be downloaded as a [precompiled library](https://github.com/tcunit/TcUnit/releases), or you can download the [source code](https://www.github.com/TcUnit/TcUnit) and compile the library yourself. ### Install from library file + If you’ve downloaded the library, you should have a file called **TcUnit.library** in your computer. Start your TwinCAT XAE (Visual Studio). In the menu of Visual Studio select **PLC** and then **Library Repository...** @@ -30,6 +32,7 @@ Click on **Install...**, locate the **TcUnit.library** file and double-click on 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. @@ -43,6 +46,7 @@ This will install the library on your computer. Once the library is installed, the file that you saved on the desktop can be removed. ### 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...** @@ -51,6 +55,7 @@ Open your TwinCAT project, and right-click on the **References** under the PLC-p Next go to the TcUnit-group, select TcUnit and click **OK**. ### Create test suites and run them + For every function block (or free function) that you have defined we want to create a test function block (test suite), which has the responsibility to: - Instantiate the FB under test @@ -78,7 +83,7 @@ Each test suite is responsible of testing one FB or function, and can have one o 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. -``` +```StructuredText FUNCTION_BLOCK FB_Sum VAR_INPUT one : UINT; @@ -92,7 +97,7 @@ END_VAR Now let’s create the test suite for this. This FB needs to extend `TcUnit.FB_TestSuite`. -``` +```StructuredText FUNCTION_BLOCK FB_Sum_Test EXTENDS TcUnit.FB_TestSuite VAR END_VAR @@ -108,7 +113,7 @@ For them well-named tests are invaluable. We’ll be creating two tests called `TwoPlusTwoEqualsFour` and `ZeroPlusZeroEqualsZero`. The `TwoPlusTwoEqualsFour` will look like this: -``` +```StructuredText METHOD TwoPlusTwoEqualsFour VAR Sum : FB_Sum; @@ -136,7 +141,7 @@ This gives the flexibility to have tests that span over more than one PLC-cycle. For `ZeroPlusZeroEqualsZero` it’s more or less the same code. -``` +```StructuredText METHOD ZeroPlusZeroEqualsZero VAR Sum : FB_Sum; @@ -156,7 +161,8 @@ TEST_FINISHED(); ``` Next we need to update the body of the test suite (`FB_Sum_Test`) to make sure these two tests are being run. -``` + +```StructuredText TwoPlusTwoEqualsFour(); ZeroPlusZeroEqualsZero(); ``` @@ -168,7 +174,7 @@ Being part of the library project we only want a convenient way to test all the `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. -``` +```StructuredText PROGRAM PRG_TEST VAR fbSum_Test : FB_Sum_Test; // This is our test suite @@ -191,7 +197,7 @@ As we can see, the test `TwoPlusTwoEqualsFour` failed, which means that the one 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`. -``` +```StructuredText FUNCTION_BLOCK FB_Sum VAR_INPUT one : UINT; @@ -217,4 +223,4 @@ Obviously this is a very simple example and the purpose of this was to show how 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](programming-example.md). -The source code for this example is [available on GitHub](https://github.com/tcunit/ExampleProjects/tree/master/SimpleExampleProject). \ No newline at end of file +The source code for this example is [available on GitHub](https://github.com/tcunit/ExampleProjects/tree/master/SimpleExampleProject). diff --git a/docs/programming-example.md b/docs/programming-example.md index f5e1643..c55cbc8 100644 --- a/docs/programming-example.md +++ b/docs/programming-example.md @@ -4,6 +4,7 @@ title: Programming example --- ## Introduction + For this example we are going to develop a TwinCAT library with some well defined functionality using test driven development with the TcUnit framework. The scope of the library will be developing functions to handle certain aspects of the IO-Link communication. IO-Link is a digital point-to-point (master and slave) serial communication protocol. @@ -37,6 +38,7 @@ This is only a description of the data on a high level, for all the details on w The number of optional parameters can be varying (zero parameters as well) depending on the diagnosis message itself. ## Data to be parsed + What we will do here is to create test cases to parse each and one of the mandatory fields. Each field will be parsed by its own function block that will provide the data above in a structured manner. Looking at the diagnosis history object, the diagnosis messages themselves are an array of bytes that are read by SDO read. For this particular example, we assume we have the stream of bytes already prepared by the SDO read. @@ -48,6 +50,7 @@ What we need to do now is to create a data structure for each of the data fields Before we start to dwell too deep into the code, it's good to know that all the source code for the complete example is available [on GitHub](https://github.com/tcunit/ExampleProjects/tree/master/AdvancedExampleProject), as it might be preferred to look at the code in the Visual Studio IDE rather than on a webpage. ### Diagnosis code + The diagnosis code looks like this: | Bit 0-15 | Bit 16-31 | @@ -61,7 +64,7 @@ The diagnosis code looks like this: We'll create a struct for it: -``` +```StructuredText TYPE ST_DIAGNOSTICCODE : STRUCT eDiagnosticCodeType : E_DIAGNOSTICCODETYPE; @@ -72,7 +75,7 @@ END_TYPE where the `E_DIAGNOSTICCODETYPE` is -``` +```StructuredText TYPE E_DIAGNOSTICCODETYPE : ( ManufacturerSpecific := 0, @@ -99,7 +102,7 @@ The flags have three parameters: "Diagnosis type", "Time stamp type", "Number of We'll create a struct for it: -``` +```StructuredText TYPE ST_FLAGS : STRUCT eDiagnostisType : E_DIAGNOSISTYPE; @@ -111,7 +114,7 @@ END_TYPE Where `E_DiagnosisType` and `E_TimeStampType` are respectively: -``` +```StructuredText TYPE E_DIAGNOSISTYPE : ( InfoMessage := 0, @@ -124,7 +127,7 @@ END_TYPE Where the `Unspecified` value is there in case we would receive one of the values that are reserved for future standardization. -``` +```StructuredText TYPE E_TIMESTAMPTYPE : ( Local := 0, @@ -138,18 +141,21 @@ The difference between the global and local timestamp is that the global is base It's interesting to store this information as you probably want to handle the reading of the timestamp differently depending on if it's a local or a global timestamp. ### Text identity + The text identity is just a simple unsigned integer (0-65535) value which is a reference to the diagnosis text file located in the ESI-xml file for the IO-Link master. This information is valuable if you want to do further analysis on the event, as it will give you more details on the event in a textual format. ### Time stamp + The 64 bit timestamp is either the EtherCAT DC-clock timestamp or the local time stamp of the EtherCAT slave/IO-Link master, depending on whether DC is enabled and/or the IO-Link master supports DC. The 64-bit value holds data with 1ns resolution. ### The complete diagnostic message + Now that we have all four diagnosis message parameters, we'll finish off by creating a structure for them that our parser will deliver as output once a new diagnosis event has occurred. Based on the information provided above it will have the following layout: -``` +```StructuredText TYPE ST_DIAGNOSTICMESSAGE : STRUCT stDiagnosticCode : ST_DIAGNOSTICCODE; @@ -176,7 +182,8 @@ As this example is quite simple, we'll solve that for every function block by ma The function blocks and their headers will have the following layout: ### Main diagnosis message event parser -``` + +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageParser VAR_INPUT anDiagnosticMessageBuffer : ARRAY[1..28] OF BYTE; @@ -190,7 +197,8 @@ This takes the 28 bytes that we receive from the IO-Link master, and outputs the Note that in this example we'll only make use of the first 16 (mandatory) bytes, and ignore the 12 (optional) bytes. ### Diagnostic code parser -``` + +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageDiagnosticCodeParser VAR_INPUT anDiagnosticCodeBuffer : ARRAY[1..4] OF BYTE; @@ -199,10 +207,12 @@ VAR_OUTPUT stDiagnosticCode : ST_DIAGNOSTICCODE; END_VAR ``` + This function block takes four of the 28 bytes as input and outputs the diagnostic code according to the layout of our struct `ST_DIAGNOSTICCODE` described earlier. ### Flags parser -``` + +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageFlagsParser VAR_INPUT anFlagsBuffer : ARRAY[1..2] OF BYTE; @@ -212,11 +222,11 @@ VAR_OUTPUT END_VAR ``` - This function block takes two of the 28 bytes as input and outputs the flags according to the layout of our struct `ST_FLAGS` described earlier. ### Text identity parser -``` + +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageTextIdentityParser VAR_INPUT anTextIdentityBuffer : ARRAY[1..2] OF BYTE; @@ -225,10 +235,12 @@ VAR_OUTPUT nTextIdentity : UINT; END_VAR ``` + This function block takes two of the 28 bytes as input and outputs the text identity as an unsigned integer according to the description earlier. ### Timestamp parser -``` + +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageTimeStampParser VAR_INPUT anTimeStampBuffer : ARRAY[1..8] OF BYTE; @@ -238,6 +250,7 @@ VAR_OUTPUT sTimeStamp : STRING(29); END_VAR ``` + This function block takes eight of the 28 bytes as input and outputs the timestamp as a human-readable string. Note that we also have a bIsLocalTime input, as we want to have different handling on the parsing of the timestamp depending on whether the timestamp is a local or global time stamp. This could be handled in many ways, but for the sake of this example we'll handle the timestamp as: @@ -255,6 +268,7 @@ Everything returned will just be with the default values of the different struct Our next step will be to write the unit tests that will make our tests fail, and once that is done (and not before!), we'll write the actual body (implementation) code for the function blocks. ## Test cases + 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. @@ -288,7 +302,7 @@ For every test that we will run we will have to do an assertion, checking whethe Let's start by creating the five test suites (orange above), and instantiating them in `PRG_TEST` and running them. -``` +```StructuredText PROGRAM PRG_TEST VAR fbDiagnosticMessageDiagnosticCodeParser_Test : FB_DiagnosticMessageDiagnosticCodeParser_Test; @@ -304,9 +318,10 @@ TcUnit.RUN(); 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 + The function block `FB_DiagnosticMessageDiagnosticCodeParser` is responsible for parsing a diagnostic code type (ManufacturerSpecific, EmergencyErrorCode or ProfileSpecific) together with the code itself. -``` +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageDiagnosticCodeParser_Test EXTENDS TcUnit.FB_TestSuite ``` @@ -318,7 +333,7 @@ We want to make sure our function block correctly parses the three different dia The first test will represent an emergency code, and the header of the method defining the test will look like follows: -``` +```StructuredText METHOD PRIVATE WhenEmergencyErrorCodeExpectEmergencyErrorCode VAR fbDiagnosticMessageDiagnosticCodeParser : FB_DiagnosticMessageDiagnosticCodeParser; @@ -364,7 +379,7 @@ The test result for the struct that the function block outputs should be `Emerge Next we move to the body of the test suite: -``` +```StructuredText TEST('WhenEmergencyErrorCodeExpectEmergencyErrorCode'); // @TEST-RUN @@ -391,7 +406,8 @@ Before doing any implementation code, we need to finish our different test cases What follows are three additional test fixtures and expected test results. **Test "`WhenManufacturerSpecificExpectManufacturerSpecific`"** -``` + +```StructuredText METHOD PRIVATE WhenManufacturerSpecificExpectManufacturerSpecific VAR fbDiagnosticMessageDiagnosticCodeParser : FB_DiagnosticMessageDiagnosticCodeParser; @@ -433,7 +449,7 @@ TEST_FINISHED(); **Test "`WhenProfileSpecificExpectProfileSpecific`"** -``` +```StructuredText METHOD PRIVATE WhenProfileSpecificExpectProfileSpecific VAR fbDiagnosticMessageDiagnosticCodeParser : FB_DiagnosticMessageDiagnosticCodeParser; @@ -473,7 +489,8 @@ TEST_FINISHED(); ``` **Test "`WhenReservedForFutureUseExpectReservedForFutureUse`"** -``` + +```StructuredText METHOD PRIVATE WhenReservedForFutureUseExpectReservedForFutureUse VAR fbDiagnosticMessageDiagnosticCodeParser : FB_DiagnosticMessageDiagnosticCodeParser; @@ -519,7 +536,7 @@ Every time we run the function block under test, we assert that the output (`stD Next we need to make sure to call all the tests in the body of the test suite. -``` +```StructuredText WhenEmergencyErrorCodeExpectEmergencyErrorCode(); WhenManufacturerSpecificExpectManufacturerSpecific(); WhenProfileSpecificExpectProfileSpecific(); @@ -527,8 +544,10 @@ WhenReservedForFutureUseExpectReservedForFutureUse(); ``` ### FB_DiagnosticMessageFlagsParser_Test + 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 test-fixtures for our tests - Declare the test-results for the test-fixtures @@ -547,6 +566,7 @@ A couple of good tests would be to try every code type (info, warning, error) an `TODO: INSERT IMAGE HERE` Let's write four tests and call them: + - `WhenErrorMessageExpectErrorMessageLocalTimestampAndFourParameters` - `WhenInfoMessageExpectInfoMessageGlobalTimestampAndZeroParameters` - `WhenReservedForFutureUseMessageExpectReservedForFutureUseMessageLocalTimestampAnd33Parameters` @@ -558,7 +578,7 @@ With all this information other developers get a lot of documentation for free! **Test "`WhenErrorMessageExpectErrorMessageLocalTimestampAndFourParameters`"** -``` +```StructuredText METHOD PRIVATE WhenErrorMessageExpectErrorMessageLocalTimestampAndFourParameters VAR fbDiagnosticMessageFlagsParser : FB_DiagnosticMessageFlagsParser; @@ -596,7 +616,8 @@ TEST_FINISHED(); ``` **Test "`WhenErrorMessageExpectErrorMessageLocalTimestampAndFourParameters`"** -``` + +```StructuredText METHOD PRIVATE WhenInfoMessageExpectInfoMessageGlobalTimestampAndZeroParameters VAR fbDiagnosticMessageFlagsParser : FB_DiagnosticMessageFlagsParser; @@ -632,9 +653,10 @@ AssertEquals(Expected := cnFlags_NumberOfParametersInDiagnosisMessageZero, TEST_FINISHED(); ``` + **Test "`WhenReservedForFutureUseMessageExpectReservedForFutureUseMessageLocalTimestampAnd33Parameters`"** -``` +```StructuredText METHOD PRIVATE WhenReservedForFutureUseMessageExpectReservedForFutureUseMessageLocalTimestampAnd33Parameters VAR fbDiagnosticMessageFlagsParser : FB_DiagnosticMessageFlagsParser; @@ -672,7 +694,8 @@ TEST_FINISHED(); ``` **Test "`WhenWarningMessageExpectWarningMessageLocalTimestampAndTwoParameters`"** -``` + +```StructuredText METHOD PRIVATE WhenWarningMessageExpectWarningMessageLocalTimestampAndTwoParameters VAR fbDiagnosticMessageFlagsParser : FB_DiagnosticMessageFlagsParser; @@ -715,13 +738,13 @@ To verify that our code outputs a diagnosis type of unspecified, we need to make 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. -``` +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageFlagsParser_Test EXTENDS TcUnit.FB_TestSuite ``` And as usual, we need to add a call to all the test-methods in the body of the test suite. -``` +```StructuredText TestWithEmergencyMessage(); TestWithManufacturerSpecificMessage(); TestWithUnspecifiedMessageMessage(); @@ -732,19 +755,21 @@ What we've got left is to create test cases for the parsing of the text identity 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. We accomplish to test the three values by changing the two bytes that make up the unsigned integer. The header of the test suite: -``` +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageTextIdentityParser_Test EXTENDS TcUnit.FB_TestSuite ``` We'll write the tests Low/Med/High, testing for the different inputs 0, 34500 and 65535. **Test "`WhenTextIdentityLowExpectTextIdentity0`"** -``` + +```StructuredText METHOD PRIVATE WhenTextIdentityLowExpectTextIdentity0 VAR fbDiagnosticMessageTextIdentityParser : FB_DiagnosticMessageTextIdentityParser; @@ -773,7 +798,8 @@ TEST_FINISHED(); ``` **Test "`WhenTextIdentityMedExpectTextIdentity34500`"** -``` + +```StructuredText METHOD PRIVATE WhenTextIdentityMedExpectTextIdentity34500 VAR fbDiagnosticMessageTextIdentityParser : FB_DiagnosticMessageTextIdentityParser; @@ -802,7 +828,8 @@ TEST_FINISHED(); ``` **Test "`WhenTextIdentityHighExpectTextIdentity65535`"** -``` + +```StructuredText METHOD PRIVATE WhenTextIdentityHighExpectTextIdentity65535 VAR fbDiagnosticMessageTextIdentityParser : FB_DiagnosticMessageTextIdentityParser; @@ -828,10 +855,12 @@ AssertEquals(Expected := cnTextIdentity_IdentityHigh, Message := 'Test $'TextIdentity#High$' failed'); TEST_FINISHED(); -``` + +```StructuredText As can be seen the only thing that varies between the tests (other than name) is the different inputs and expected output. ### 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 from the current task time (the local clock could be extracted from the EtherCAT-slave, but for the sake of simplicity we'll use the task DC). 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. @@ -839,12 +868,13 @@ What this means is that if the timestamp is local, the eight bytes don't matter 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. -``` +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageTimeStampParser_Test EXTENDS TcUnit.FB_TestSuite ``` + Let's create our tests, and start with the test * **"`TestWithTimestampZeroTimeExpectCurrentTime`"**. -``` +```StructuredText METHOD PRIVATE TestWithTimestampZeroTimeExpectCurrentTime VAR fbDiagnosticMessageTimeStampParser : FB_DiagnosticMessageTimeStampParser; @@ -875,7 +905,7 @@ TEST_FINISHED(); **Test "`TestWithValidTimestampExpectSameTimestamp`"** -``` +```StructuredText METHOD PRIVATE TestWithValidTimestampExpectSameTimestamp VAR fbDiagnosticMessageTimeStampParser : FB_DiagnosticMessageTimeStampParser; @@ -925,6 +955,7 @@ Because the `T_DCTIME64`-type that is returned from `F_GetActualDcTime64()` is a Note that the assertion of the local time stamp is based on getting the current DC-task time by utilizing the [`F_GetCurDcTaskTime64()`](https://infosys.beckhoff.com/index.php?content=../content/1031/tcplclib_tc2_ethercat/2268414091.html&id=), 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. @@ -937,7 +968,7 @@ The code is available on [GitHub](https://github.com/tcunit/ExampleProjects/tree We'll go through all the details, thus it should thus be easy for you to add any test cases that you find necessary. As usual, header first: -``` +```StructuredText FUNCTION_BLOCK FB_DiagnosticMessageParser_Test EXTENDS TcUnit.FB_TestSuite ``` @@ -951,7 +982,7 @@ We want to make sure to test various types of diagnostic messages, with their co Because this function uses the other four function blocks, we need to create a complete structure for every test with the complete content of a diagnosis message, making the tests prerequisites for every test quite large. We'll start with the test **`TestWithEmergencyMessage`**. -``` +```StructuredText METHOD PRIVATE TestWithEmergencyMessage VAR fbDiagnosticMessageParser : FB_DiagnosticMessageParser; @@ -1048,7 +1079,7 @@ TEST_FINISHED(); Next up is testcase **`TestWithManufacturerSpecificMessage`** -``` +```StructuredText METHOD PRIVATE TestWithManufacturerSpecificMessage VAR fbDiagnosticMessageParser : FB_DiagnosticMessageParser; @@ -1158,7 +1189,7 @@ TEST_FINISHED(); And finally two test cases where the diagnosis type is unspecified. -``` +```StructuredText METHOD PRIVATE TestWithUnspecifiedMessageMessage VAR fbDiagnosticMessageParser : FB_DiagnosticMessageParser; @@ -1268,7 +1299,7 @@ TEST_FINISHED(); And a second variant with some different input parameters. -``` +```StructuredText METHOD PRIVATE TestWithUnspecifiedMessageMessage_ParameterVariant VAR fbDiagnosticMessageParser : FB_DiagnosticMessageParser; @@ -1392,14 +1423,15 @@ Done any change to your code? Just re-run the tests and make sure you haven’t broken anything. Fantastic, isn’t it? -We’ve written a total of 17 tests, so now let’s build the project and run the tests. +We've written a total of 17 tests, so now let’s build the project and run the tests. + +![TcUnit many fails](img/TcUnitManyFails.png) -**TODO: Image here** This is just a selection of all the failed asserts. For every failed assert, we can see the expected value we should have got in case the implementing code did what it is supposed to do and also the actual value as well as the message that we provided to the assert. The statistics are printed a little bit further down: -**TODO: Image here** +![TcUnit 16 of 17 failed](img/TcUnit16Of17Failed_2.png) First we can see that we have had five test suites running, in where each had a certain amount of tests defined. Every test suite is responsible to test a specific function block. @@ -1408,7 +1440,7 @@ But how come that we’ve had a successful test even though we haven’t yet wri This is usually related to tests that test some zero-values, where the default return value of the function block under test is zero. In this case it is this test: -``` +```StructuredText METHOD PRIVATE WhenTextIdentityLowExpectTextIdentity0 VAR fbDiagnosticMessageTextIdentityParser : FB_DiagnosticMessageTextIdentityParser; @@ -1424,6 +1456,6 @@ VAR END_VAR ``` -We’re testing the function block `FB_DiagnosticMessageTextIdentityParser` by providing it with a zero-value as input (two bytes, each holding `0x00`) and expecting the number 0 as result. +We're testing the function block `FB_DiagnosticMessageTextIdentityParser` by providing it with a zero-value as input (two bytes, each holding `0x00`) and expecting the number 0 as result. The default initialization of a number value in TwinCAT is always zero, and thus this is returned which makes our test succeed. Tests that pass without implementing code generally don't provide much value. diff --git a/docs/unit-testing-concepts.md b/docs/unit-testing-concepts.md index 090a462..23bab67 100644 --- a/docs/unit-testing-concepts.md +++ b/docs/unit-testing-concepts.md @@ -9,6 +9,7 @@ This page briefly describes test driven development in general, and some basic c - [Unit testing with TcUnit](#unit-testing-with-tcunit) ## Test driven development + Test driven development (TDD) is the practice of writing a test for some required functionality, before writing any implementation code. The idea is that when you run your tests the first time, they will fail. After you’ve written your (failing) tests, you do the actual implementation, until the tests succeed. @@ -46,6 +47,7 @@ It’s the starting ramp-up time to get into the “TDD thinking” that takes t Writing tests costs time, but overall development takes less time. ## Unit testing with TcUnit + As TcUnit is a xUnit type of framework, and as such has certain components with their respective responsibility. ![TcUnit blocks](img/tcunit-block-explanations.png) @@ -53,35 +55,42 @@ As TcUnit is a xUnit type of framework, and as such has certain components with Let’s go through each and one of them. ### PRG_Test + **PRG_Test** is not part of the framework or the xUnit-concept at all, but is just here to point out that the test cases need an environment to run in. They need to be initialized. The program that will execute the tests will never be executed in a production environment, and is only here for the sole purpose of running the unit tests. It can either be included directly in your library projects or as a completely separate solution. ### Test case + A test case is the most rudimentary building block. It is defined by a name that describes the intention of the test and the expected result. ### Test fixture + A test fixture is the set of preconditions or state needed to run a test. In TcUnit these can typically be some constant variables that are used as input for a function block under test. In TcUnit these can be declared in the test suite or in the test case, depending on whether you want them to be shared across several tests or not. ### Test suite + A test suite is a collection of tests related to each other. In TcUnit they can share the same test fixture, but there is also a possibility to have a separate test fixture for each test in a test suite. In TcUnit a test suite is defined as a function block. ### Assertions + An assertion is a method that verifies some state of the unit under test. If you for instance want to verify that a boolean output of a function block is according to a certain state, you can assert that the output is true. There are many various assert-methods in TcUnit, depending on the data type that holds the state. Some examples are `AssertEquals_INT`, `AssertEquals_STRING` and `AssertEquals_BOOL`. ### Test result formatter + This is a part that produces the end result of the test, that for instance can be used for a human to read. ![Test results](img/test-results.png) ### Test runner -The test runner of TcUnit makes sure to run all the tests defined and collect the results from them. \ No newline at end of file + +The test runner of TcUnit makes sure to run all the tests defined and collect the results from them.