Unit Testing

Introduction

In the tutorials so far we have introduced many features of Flightkit which you can use to produce spacecraft flight software, but we haven’t yet discussed mechanisms you can use to ensure the code you build using Flightkit is correct.

When making use of reusable software in space systems (where the consequences of failure can be painful!), it’s important that you, and others, can have confidence that the software will work as intended.

To help with this, the Flightkit Tooling has Unit Testing support built into it.

This tutorial will show you how to use this Unit Testing support when developing your own Component Types.

In this tutorial, you will:

  • Learn how to use Flightkit Tooling to generate Unit Test files for a given Component Type.

  • Learn about the basic structure used for Unit Testing Component Types.

  • Create, implement and run some Unit Tests for the SimpleImager Component Type you worked on in the previous tutorial.

Before You Begin

Guidance on the styles and conventions used in these tutorials is available here.

Unless otherwise directed, all commands in this tutorial must be run in the developing_components/UnitTesting workspace.

You must have Flightkit installed as described in the Getting Started Guide.

Navigate to the developing_components/UnitTesting workspace in a terminal and run hfk workspace prepare to ensure it is ready to use.

Unity and CMock

Flightkit uses two third-party tools to assist with developing Unit Tests:

  • Unity, a Unit Test framework built for embedded C applications, and

  • CMock, a mock generator designed to work with Unity.

Unity provides helper functions and a set of assertions for composing Unit Tests.

CMock is used to generate mocks. These are functions used to fulfil the requirements of the code being tested in place of the real functions, which are often unavailable when Unit Testing.

For example, when working on code which commands deployment of a solar array, it would be preferable to mock the function responsible for handling that command. CMock can be used to check that the function is called correctly without requiring the code, or the attached solar array hardware, to be present. With the correct selection of mocks, code which is dependent on embedded hardware can be tested in a low risk environment - like on your Linux development machine.

There is a wealth of information about Unit Testing available online. In this tutorial we assume a basic familiarity with the concepts involved, and focus on how to write and run tests when working with Flightkit.

We will cover various Unity and CMock features we often use, and where relevant we will link to the documentation provided by those projects.

Unit Tests in Flightkit

In Flightkit Component Types are the principle unit of reuse. This means that it is reasonable to expect a given Component Type to work correctly when instantiated within any Deployment Type.

To try and ensure that a Component Type will work correctly in new contexts, we typically write a set of tests which attempt to cover all of the interfaces of a Component Type.

This usually means:

  • For Service Providers, writing tests which make sure they implement each Operation of that Service correctly.

  • For Component Types with Actions and Parameters, writing tests which invoke each Action with various Arguments, and get and set each Parameter with various values, to make sure each case is handled correctly.

  • For Service Consumers, mocking the Service Operations which the Component Type issues. This gives us opportunity to make sure the correct Service Operations are issued at the correct times.

For complex Component Types there are more interfaces to consider, but they follow the same principles: exercise your inputs, and mock your requirements.

Testing SimpleImager

In the following sections you will create, implement and run a Unit Test to demonstrate the SimpleImager Component Type's loadSettings Action, which you implemented in the previous tutorial.

If you haven’t yet completed that tutorial, we suggest you do so now.

Creating the Test File

Generating the test files for a Component Type is very simple. Whenever a Component Type is generated using the generate command, test files are automatically created.

A complete copy of the SimpleImager Component Type is provided in this tutorial’s library. This is the Component Type you will write the Unit Tests against.

Generate a test file for the provided SimpleImager Component Type by generating it.

hfk component-type generate SimpleImager

This creates a test file for the SimpleImager Component Type in the following location:

library/component_types/SimpleImager/test/SimpleImager_Test.c

Open SimpleImager_Test.c.

As you can see, SimpleImager_Test.c already contains various data structures and functions required to write some Unit Tests. There are TODO comments indicating where you will need to make changes. There are also, at the bottom, a number of autogenerated Unit Tests which the Flightkit Tooling has created.

From top to bottom, the file includes:

  • A set of structures which are used instead of the usual Component management mechanisms of a Deployment. Importantly, gt_InitData is a structure which can be used to provide different Initialisation Data values to SimpleImager, and gpt_SimpleImager is the global pointer to the SimpleImager Component Instance which must be passed when calling its public functions.

  • Next is an empty section for placing helper functions. If you need to provide stubs to CMock, you can define them here.

  • Below that are the setUp() and tearDown() functions. These functions are called by the Unity test framework before and after every test, respectively. By default these are generated such that they call the Component Type's local initialisation and finalisation functions. Any other required set up and tear down steps can be added here.

  • The remaining functions are the tests themselves. The test functions for SimpleImager all begin with SimpleImager_Test_. Unity calls each of the functions with this prefix in order, from top to bottom. Flightkit Tooling generates some test functions for you automatically based on the Component Type's componentType.xml. The first one, SimpleImager_Test_initAndFini(), verifies the Component Type's initialisation and finalisation functions perform as expected. The rest verify that the Component Type's Actions and Parameter accessors perform as expected in certain scenarios.

Notice that the autogenerated tests are only partially complete. If you wish to make use of them, you will have to complete their implementation according to their TODO comments.

It is also worth noting that they are not a comprehensive list. If your aim is to achieve full test coverage, you will have to supplement them with additional tests covering the scenarios the Flightkit Tooling cannot infer.

The following sections of the tutorial demonstrate this process through the design and implementation of four new tests.

Designing the Tests

As is the case with any software, it makes sense to think about how you’re going to approach writing your Unit Tests before you begin.

Tests should usually cover the broad functional jobs which a Component Type fulfils. In the case of SimpleImager, we are mostly interested in testing the code which uses FSS which you added in the previous tutorial.

To test the settings reading and parsing functionality, you will implement unit tests which exercise the following specific cases:

  1. Trying to capture an image before any settings are loaded.

  2. Trying to read settings from a file which doesn’t exist.

  3. Trying to read settings from a file which contains malformed settings. There are many possible ways the settings data can be malformed, but you will just write a single test covering one kind of error.

  4. Reading settings from a well formed file, and checking that the settings are correctly presented in the SimpleImager Parameters.

We will work through this list in order in the remaining sections of the tutorial, referring back to it as required.

Initialisation Data

The first TODO in SimpleImager_Test.c suggests you set Initialisation Data values for the Component Instance under test. However, as you can see in its main header file - library/component_types/SimpleImager/include/SimpleImager.h - SimpleImager does not contain any Initialisation Data.

Update the comment to indicate that no Initialisation Data is required.

Show the updated TODO
/** The initialisation data for the component */
static SimpleImager_Init_t gt_InitData =
{
-   /* TODO: Set initial values for initialisation data */
+   /* SimpleImager does not have any initialisation data */
};

When testing other Component Types you may wish to put invalid values in gt_InitData in order to test validation. You may also want to provide different Initialisation Data for different tests. This is why gt_InitData is not const by default.

Test 1: Capture Without Settings

The first test outlined above needs to check that invoking captureImage before settings are loaded is handled correctly.

Rather than creating this test from scratch, you can instead use the autogenerated SimpleImager_Test_captureImageSuccessful() function as a starting point since it is not required for this tutorial.

The first step in implementing the new test is to select a name for it. It is important to ensure that test functions are named in a way which clearly describes the test being carried out. This allows readers to quickly understand the purpose of each test, whether they are reading the test functions themselves, or the test output produced when the tests are run. This test function will test capturing an image without loading settings, so its name should reflect that.

Rename the SimpleImager_Test_captureImageSuccessful() test function to have a more appropriate name for the test you are implementing, and update the description comment accordingly.

Make sure to preserve the SimpleImager_Test_ prefix in the name so that Unity can run the test.

Show the updated test function name
-/** Test successfully invoking the captureImage action */
-void SimpleImager_Test_captureImageSuccessful(void)
+/** Test that captureImage fails if invoked before settings are loaded */
+void SimpleImager_Test_captureImageFailureWithoutSettings(void)
{
    status_t t_Status;              /* The current status */

    /* TODO: Set up any expected external function calls for this action */

    t_Status = SimpleImager_captureImage(gpt_SimpleImager);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

We’ll show you the code needed for this first test, but you might like to try and work it out on your own first.

If so, look at how the code already generated in SimpleImager_Test.c is written, and take a look at the various assertions which Unity provides. These are documented by Unity on Github.

A good approach to writing Unit Tests is to first establish pre-conditions for the test, then carry out some steps which may or may not fail, and then examine the results of those steps.

The pre-condition for this test is that no settings are currently loaded into the Component Instance. You can check whether this is the case using the settingsLoaded parameter.

In the SimpleImager_Test_captureImageFailureWithoutSettings() function, get the settingsLoaded Parameter and confirm it is false.

Show the added Parameter get and test assertion
/** Test that captureImage fails if invoked before settings are loaded */
void SimpleImager_Test_captureImageFailureWithoutSettings(void)
{
    status_t t_Status;              /* The current status */
+   bool b_SettingsLoaded;          /* Whether the settings are loaded */

-   /* TODO: Set up any expected external function calls for this action */
+   t_Status = SimpleImager_getSettingsLoaded(gpt_SimpleImager, &b_SettingsLoaded);
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_FALSE(b_SettingsLoaded);

    t_Status = SimpleImager_captureImage(gpt_SimpleImager);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

Finally, make sure that invoking the capture Action fails.

To do this, change the autogenerated assertion so it requires t_Status to not equal STATUS_SUCCESS.

Show the updated t_Status assertion.
/** Test that captureImage fails if invoked before settings are loaded */
void SimpleImager_Test_captureImageFailureWithoutSettings(void)
{
    status_t t_Status;              /* The current status */
    bool b_SettingsLoaded;          /* Whether the settings are loaded */

    t_Status = SimpleImager_getSettingsLoaded(gpt_SimpleImager, &b_SettingsLoaded);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
    TEST_ASSERT_FALSE(b_SettingsLoaded);

    t_Status = SimpleImager_captureImage(gpt_SimpleImager);
-   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_NOT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

Running Tests

Building and running tests is built into the Flightkit Tooling.

Build and run the SimpleImager Unit Tests by running the following command:

hfk component-type test SimpleImager

You should see output for each test executed, and a summary of how many failed.

Confirm that you see SimpleImager_Test_captureImageFailureWithoutSettings:PASS in the output.

You should also see error output from SimpleImager reporting that loadSettings must be invoked before captureImage. This indicates that the SimpleImager_Test_captureImageFailureWithoutSettings() test function is testing what you expect, and SimpleImager is behaving as it should.

Don’t worry about the other tests failing. In the following sections you will add the remainder of the tests outlined above.

Tests Using Mocks

We’ll now guide you through writing tests which mock the FSS functions which SimpleImager calls. By mocking these functions, you do not need to connect an FSS Provider to your Component Instance. This ensures that as little effort as possible is expended testing or debugging code which is not part of the Component Type under test.

Generating Mocks

CMock generates mocking macros and functions for use in your Unit Test. The macros can validate input arguments to mocked functions, populate output arguments, and generate specific return values. If these macros are not powerful enough, CMock allows you to provide callbacks to use in place of the mocked functions at runtime.

The Flightkit Tooling controls what mocks are generated for a particular Component Type under test. Specifically:

  • Functions which the Component Type uses to access required Services are mocked.

  • Functions defined by required Component Types are mocked.

  • Functions which the Component Type uses to store configuration are mocked.

In this tutorial you will only need to use mocks for the first group of functions: those which access required Services.

Component Types may require Modules to access additional functionality. These are buildable artefacts which can contain types and functions, amongst other things.

We discuss Modules in more detail in a later tutorial. It is worth noting at this stage, however, that functions defined by required Modules are not mocked.

Having built and run the unit tests earlier, the mocks for the SimpleImager Component Type have already been generated.

Check that MockSimpleImager_FSS_package.h has been generated at the following location:

generated/library/component_types/SimpleImager/test/mocks/container/MockSimpleImager_FSS_package.h

This file is under the generated/ directory. You should not make changes to code in this directory tree because Flightkit Tooling will overwrite them when generating code. When using version control systems you should not commit the generated/ directory. It is typically not even necessary to look at generated code, but it can sometimes be helpful.

CMock prefixes mocked file names with Mock. This is so that, at build time, the mocked functions can be distinguished from the real functions. It also means that in the test file you must use the correct file name in order to access the generated mocks.

Check that container/MockSimpleImager_FSS_package.h has been included at the top of SimpleImager_Test.c:

#include "container/MockSimpleImager_FSS_package.h"

Open the MockSimpleImager_FSS_package.h mock header.

This file contains a lot of content generated by CMock, and we won’t discuss it in detail. It is, however, worth looking at an example of the macros which are available.

Find the SimpleImager_FSS_openFileFs_ExpectAndReturn() macro.

This macro instructs CMock to:

  • Expect a call to SimpleImager_FSS_openFileFs(),

  • Check the input Arguments, and

  • Return with the given status.

SimpleImager_FSS_openFileFs() is the function which SimpleImager calls when it needs to open a file. By mocking it, you can check that SimpleImager is opening the correct file in the correct way.

We will not go further into the specifics of what CMock generates. In the following sections you will see how some of these mocks work by example. If you would like to read about them in more detail, documentation is available in the CMock Github repository.

Test 2: Settings File Not Found

As outlined above, the next test to implement should check that loading settings from a file which doesn’t exist fails gracefully. For this, you will have to make use of mocks.

The loadSettings Action issues an openFile FSS Operation to its fs service requirement by calling SimpleImager_FSS_openFileFs(). As mentioned earlier, the Flightkit Tooling generates mock functions for all service requirements, so you will need to tell CMock to expect this call within your test using SimpleImager_FSS_openFileFs_ExpectAndReturn().

When setting up the expected call, you will need to either validate or ignore each of the input Arguments depending on what you expect to be passed into it. For this test, pt_Timeout and pt_SelectionID are not required, so can be ignored. z_FileName can also be ignored as you require the function call to report that the file was not found regardless of which file SimpleImager tries to open.

As with before, rather than writing this test from scratch, you can just modify the existing SimpleImager_Test_loadSettingsSuccessfulWithArg() test since that test is not required for this tutorial.

Modify the SimpleImager_Test_loadSettingsSuccessfulWithArg() test’s name and description so that it describes the test you are implementing. Again, make sure to preserve the SimpleImager_Test_ prefix.

Show the updated test function name
-/** Test successfully invoking the loadSettings action with an argument */
-void SimpleImager_Test_loadSettingsSuccessfulWithArg(void)
+/** Test that loadSettings fails when opening a non-existent file */
+void SimpleImager_Test_loadSettingsFailureFileNotFound(void)
{
    status_t t_Status;              /* The current status */
    uint8_t ru8_SettingsPath[SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE] = { 0 };

    /* TODO Set the ru8_SettingsPath argument */

    /* TODO: Set up any expected external function calls for this action */

    t_Status = SimpleImager_loadSettings(
        gpt_SimpleImager,
        &ru8_SettingsPath[0],
        SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

With the test function renamed, you can now resolve the TODO comments such that it performs as described above.

The first TODO can simply be deleted as the mock will ignore the file path regardless of its value.

The second TODO requires you to use the SimpleImager_FSS_openFileFs_ExpectAndReturn() function to indicate to CMock that you expect the SimpleImager_FSS_openFileFs() mock to be called. You will need to set the status returned by this function to FSS_STATUS_FILE_NOT_FOUND since you want the mock to simulate the file not being found.

Update the test to expect a call to SimpleImager_FSS_openFileFs().

Show the updated test.
/** Test that loadSettings fails when opening a non-existent file */
void SimpleImager_Test_loadSettingsFailureFileNotFound(void)
{
    status_t t_Status;              /* The current status */
    uint8_t ru8_SettingsPath[SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE] = { 0 };

-   /* TODO Set the ru8_SettingsPath argument */
-
-   /* TODO: Set up any expected external function calls for this action */
+   SimpleImager_FSS_openFileFs_ExpectAndReturn(
+       gpt_SimpleImager,
+       NULL,
+       NULL,
+       false,
+       false,
+       FSS_ACCESSMODE_READ_ONLY,
+       NULL,
+       FSS_STATUS_FILE_NOT_FOUND);
+   /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
+   SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
+   SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
+   SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();

    t_Status = SimpleImager_loadSettings(
        gpt_SimpleImager,
        &ru8_SettingsPath[0],
        SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

With the TODO`s complete, the last step is to update the `SimpleImager_loadSettings() status assertion to verify that FSS_STATUS_FILE_NOT_FOUND is returned.

Update the SimpleImager_loadSettings() status assertion to validate against the correct return status.

Show updated t_Status assertion.
/** Test that loadSettings fails when opening a non-existent file */
void SimpleImager_Test_loadSettingsFailureFileNotFound(void)
{
    status_t t_Status;              /* The current status */
    uint8_t ru8_SettingsPath[SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE] = { 0 };

     SimpleImager_FSS_openFileFs_ExpectAndReturn(
         gpt_SimpleImager,
         NULL,
         NULL,
         false,
         false,
         FSS_ACCESSMODE_READ_ONLY,
         NULL,
         FSS_STATUS_FILE_NOT_FOUND);
     /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
     SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
     SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
     SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();

    t_Status = SimpleImager_loadSettings(
        gpt_SimpleImager,
        &ru8_SettingsPath[0],
        SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE);
-   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT(FSS_STATUS_FILE_NOT_FOUND, t_Status);
}

Note that all mocks need to be configured before the Component Instance's code is called. That is why the word "expect" is used - you are telling CMock what it should expect to happen next.

With the test complete, you can now build and run all tests again to check if the new test passes.

Build and run the SimpleImager tests and confirm that SimpleImager_Test_loadSettingsFailureFileNotFound() passes.

As before, run the tests with the following command:

hfk component-type test SimpleImager

Test 3: Malformed Settings Data

The next test listed above is a little more complicated to implement. For this test, you need to check that SimpleImager correctly handles files which contain badly formatted settings.

To do this you will need to use CMock to report success from the openFile and readFile FSS Operations, whilst returning data from readFile which is malformed. You will also need to expect a call to closeFile once SimpleImager is done reading the file.

Rather than modifying an existing test to implement this new test as you have done previously, you can instead write it from scratch.

The following steps will walk you through implementing this test from start to finish.

Add a new SimpleImager_Test_loadSettingsFailureMalformedData() test directly below the SimpleImager_Test_loadSettingsFailureFileNotFound() test.

Use SimpleImager_FSS_openFileFs_ExpectAndReturn() to expect the openFile FSS Operation.

Set the returned status to STATUS_SUCCESS since you want the mock to indicate that the file was opened successfully.

Although the value of pt_SelectionID can be ignored, you will still need to configure the mock to return a file handle to SimpleImager via this argument so that SimpleImager can later use it to read from the file. You will need to create a local Service_SelectionID_t variable to implement this behaviour.

Show the new test.
+/** Test that loadSettings fails when reading a malformed settings file */
+void SimpleImager_Test_loadSettingsFailureMalformedData(void)
+{
+    /* A dummy selection ID to use as a file handle */
+    Service_SelectionID_t t_File = (Service_SelectionID_t)99;
+
+    SimpleImager_FSS_openFileFs_ExpectAndReturn(
+        gpt_SimpleImager,
+        NULL,
+        NULL,
+        false,
+        false,
+        FSS_ACCESSMODE_READ_ONLY,
+        NULL,
+        STATUS_SUCCESS);
+    /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
+    SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
+    SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
+    SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
+    /* Return a dummy test value to SimpleImager through pt_SelectionID */
+    SimpleImager_FSS_openFileFs_ReturnThruPtr_pt_SelectionID(&t_File);
+}

Next, you need to expect SimpleImager to read from the file it has "opened". This is achieved using the SimpleImager_FSS_readFileFs_ExpectAndReturn() CMock macro.

Use SimpleImager_FSS_readFileFs_ExpectAndReturn() to expect the readFile FSS Operation.

Set the t_SelectionID argument to the same value that you returned via the pt_SelectionID argument in the SimpleImager_FSS_openFileFs_ExpectAndReturn mock in the previous step. This will validate that SimpleImager is trying to read from the same file it opened.

Use SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data() and SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength() to return some malformed data, and the length of that data.

Show the second mock setup added to the new test.
/** Test that loadSettings fails when reading a malformed settings file */
void SimpleImager_Test_loadSettingsFailureMalformedData(void)
{
    /* A dummy selection ID to use as a file handle */
    Service_SelectionID_t t_File = (Service_SelectionID_t)99;
+
+   /* Some malformed data */
+   uint8_t ru8_MalformedData[] = "Malformed Data";
+   uint32_t u32_MalformedDataLength = ARRAY_COUNT(ru8_MalformedData);

    SimpleImager_FSS_openFileFs_ExpectAndReturn(
        gpt_SimpleImager,
        NULL,
        NULL,
        false,
        false,
        FSS_ACCESSMODE_READ_ONLY,
        NULL,
        STATUS_SUCCESS);
    /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
    SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
    /* Return a dummy test value to SimpleImager through pt_SelectionID */
    SimpleImager_FSS_openFileFs_ReturnThruPtr_pt_SelectionID(&t_File);
+
+   /* Expect SimpleImager to read from the opened file */
+   SimpleImager_FSS_readFileFs_ExpectAndReturn(
+       gpt_SimpleImager,
+       NULL,
+       t_File,
+       NULL,
+       NULL,
+       0,
+       STATUS_SUCCESS);
+   /* Ignore the values of pt_Timeout, pu8_Data, pu32_DataLength and
+    * u32_ReadLength */
+   SimpleImager_FSS_readFileFs_IgnoreArg_pt_Timeout();
+   SimpleImager_FSS_readFileFs_IgnoreArg_pu8_Data();
+   SimpleImager_FSS_readFileFs_IgnoreArg_pu32_DataLength();
+   SimpleImager_FSS_readFileFs_IgnoreArg_u32_ReadLength();
+
+   /* Return the malformed data and its length through the right pointers */
+   SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data(
+       &ru8_MalformedData[0], u32_MalformedDataLength);
+   SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength(
+       &u32_MalformedDataLength);
}

The above additions ignore the length arguments passed to the readFile Operation. This means the test does not check that SimpleImager is reading a sane amount of data into a correctly sized buffer.

To catch issues like this a callback would need to be written and provided to CMock using the SimpleImager_FSS_readFileFs_StubWithCallback() macro. This is outside the scope of this tutorial.

After SimpleImager reads the settings file, it must close it. You can tell CMock to expect this call by using SimpleImager_FSS_closeFileFs_ExpectAndReturn().

Expect SimpleImager_FSS_closeFileFs_ExpectAndReturn() to be called, making sure SimpleImager closes the same file it opened.

Show the added closeFile mock setup.
    /* Return the malformed data and its length through the right pointers */
    SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data(
        &ru8_MalformedData[0], u32_MalformedDataLength);
    SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength(
        &u32_MalformedDataLength);
+
+   /* Expect SimpleImager to then close the file */
+   SimpleImager_FSS_closeFileFs_ExpectAndReturn(
+       gpt_SimpleImager, NULL, t_File, STATUS_SUCCESS);
+   SimpleImager_FSS_closeFileFs_IgnoreArg_pt_Timeout();
}

You’ve now set up all the expected FSS Operations so all that’s left to do in this test is to invoke the loadSettings Action and validate the returned status.

Add a call to SimpleImager_loadSettings() and validate the returned status.

Show the added t_Status and z_FileName variables
/** Test that loadSettings fails when reading a malformed settings file */
void SimpleImager_Test_loadSettingsFailureMalformedData(void)
{
    /* A dummy selection ID to use as a file handle */
    Service_SelectionID_t t_File = (Service_SelectionID_t)99;

    /* Some malformed data */
    uint8_t ru8_MalformedData[] = "Malformed Data";
    uint32_t u32_MalformedDataLength = ARRAY_COUNT(ru8_MalformedData);
+
+   status_t t_Status;
+   cstring_t z_FileName = "/this/file/is/malformed";

    SimpleImager_FSS_openFileFs_ExpectAndReturn(
        gpt_SimpleImager,
        NULL,
Show the added Action invocation and t_Status assertion.
    /* Expect SimpleImager to then close the file */
    SimpleImager_FSS_closeFileFs_ExpectAndReturn(
        gpt_SimpleImager, NULL, t_File, STATUS_SUCCESS);
    SimpleImager_FSS_closeFileFs_IgnoreArg_pt_Timeout();
+
+   t_Status = SimpleImager_loadSettings(
+       gpt_SimpleImager, (const uint8_t *)z_FileName, strlen(z_FileName));
+   TEST_ASSERT_NOT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

The above code snippet uses the strlen() function, so you’ll need to include the <string.h> header before building the tests.

Add <string.h> to the headers included at the top of SimpleImager_Test.c.

Show the added #include directive.
#include "container/MockSimpleImager_FSS_package.h"
#include "util/Util_Log.h"
+#include <string.h>

/*---------------------------------------------------------------------------*
 * Global variables
 *---------------------------------------------------------------------------*/

Build and run the SimpleImager tests and confirm that SimpleImager_Test_loadSettingsFailureMalformedData() passes.

In the next section you’ll write the final test discussed above. In doing so you’ll reuse the code in SimpleImager_Test_loadSettingsFailureMalformedData() so that the Unit Test remains maintainable.

Helper Functions

Up to this point the Unit Tests you have written for SimpleImager are each fairly independent. There isn’t very much overlap in the contents of each test function. This is good - it makes the tests maintainable. If something changes in SimpleImager, you typically wouldn’t need to make the same changes in lots of tests.

The final test you’re going to implement, however, is very similar to SimpleImager_Test_loadSettingsFailureMalformedData(). Rather than giving SimpleImager malformed settings data, you’ll give it valid settings data, then check the parsed Parameter values.

To do this in a maintainable way, you will define a helper function for testing the loadSettings Action with an arbitrary string.

Define loadSettingsFromString()

Under the Helper functions banner, define a loadSettingsFromString() function using the body of the SimpleImager_Test_loadSettingsFailureMalformedData() test.

Use the return value from the SimpleImager_loadSettings() function call as the return value from loadSettingsFromString().

This allows tests which call loadSettingsFromString() to check the returned status code themselves, depending on what they expect.

Rather than using local variables to define the settings data, give the loadSettingsFromString() function a z_SettingsData argument.

Derive the length of the settings data to pass to the mocked FSS read using strlen(z_SettingsData).

Make sure any comments and strings copied from SimpleImager_Test_loadSettingsFailureMalformedData() are updated in the more generic loadSettingsFromString() function.

These refactoring steps should result in a function similar to that given below. If you’ve run into problems defining loadSettingsFromString() you can paste the following function into SimpleImager_Test.c under the Helper functions banner.

Show the definition of the loadSettingsFromString() helper function.
/**
 * Test the loadSettings action using the given settings data
 * @param z_SettingsData The settings data to load
 * @return The status returned from the loadSettings action
 */
static status_t loadSettingsFromString
(
    cstring_t z_SettingsData
)
{
    /* A dummy selection ID to use as a file handle */
    Service_SelectionID_t t_File = (Service_SelectionID_t)99;

    /* Get the length of the settings string */
    uint32_t u32_SettingsDataLength = strlen(z_SettingsData);

    cstring_t z_FileName = "/dummy/file/path";

    SimpleImager_FSS_openFileFs_ExpectAndReturn(
        gpt_SimpleImager,
        NULL,
        NULL,
        false,
        false,
        FSS_ACCESSMODE_READ_ONLY,
        NULL,
        STATUS_SUCCESS);
    /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
    SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
    /* Return a dummy test value to SimpleImager through pt_SelectionID */
    SimpleImager_FSS_openFileFs_ReturnThruPtr_pt_SelectionID(&t_File);

    /* Expect SimpleImager to read from the opened file */
    SimpleImager_FSS_readFileFs_ExpectAndReturn(
        gpt_SimpleImager,
        NULL,
        t_File,
        NULL,
        NULL,
        0,
        STATUS_SUCCESS);
    /* Ignore the values of pt_Timeout, pu8_Data, pu32_DataLength and
     * u32_ReadLength */
    SimpleImager_FSS_readFileFs_IgnoreArg_pt_Timeout();
    SimpleImager_FSS_readFileFs_IgnoreArg_pu8_Data();
    SimpleImager_FSS_readFileFs_IgnoreArg_pu32_DataLength();
    SimpleImager_FSS_readFileFs_IgnoreArg_u32_ReadLength();

    /* Return the data and its length through the right pointers */
    SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data(
        (const uint8_t *)z_SettingsData, u32_SettingsDataLength);
    SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength(
        &u32_SettingsDataLength);

    /* Expect SimpleImager to then close the file */
    SimpleImager_FSS_closeFileFs_ExpectAndReturn(
        gpt_SimpleImager, NULL, t_File, STATUS_SUCCESS);
    SimpleImager_FSS_closeFileFs_IgnoreArg_pt_Timeout();

    return SimpleImager_loadSettings(
        gpt_SimpleImager, (const uint8_t *)z_FileName, strlen(z_FileName));
}

Now use loadSettingsFromString() to implement the SimpleImager_Test_loadSettingsFailureMalformedData() test.

Show the use of loadSettingsFromString() in SimpleImager_Test_loadSettingsFailureMalformedData().
/** Test that loadSettings fails when reading a malformed settings file */
void SimpleImager_Test_loadSettingsFailureMalformedData(void)
{
-   /* A dummy selection ID to use as a file handle */
-   Service_SelectionID_t t_File = (Service_SelectionID_t)99;
-
-   /* Some malformed data */
-   uint8_t ru8_MalformedData[] = "Malformed Data";
-   uint32_t u32_MalformedDataLength = ARRAY_COUNT(ru8_MalformedData);
-
-   status_t t_Status;
-   cstring_t z_FileName = "/this/file/is/malformed";
-
-   SimpleImager_FSS_openFileFs_ExpectAndReturn(
-       gpt_SimpleImager,
-       NULL,
-       NULL,
-       false,
-       false,
-       FSS_ACCESSMODE_READ_ONLY,
-       NULL,
-       STATUS_SUCCESS);
-   /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
-   SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
-   SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
-   SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
-   /* Return a dummy test value to SimpleImager through pt_SelectionID */
-   SimpleImager_FSS_openFileFs_ReturnThruPtr_pt_SelectionID(&t_File);
-
-   /* Expect SimpleImager to read from the opened file */
-   SimpleImager_FSS_readFileFs_ExpectAndReturn(
-       gpt_SimpleImager,
-       NULL,
-       t_File,
-       NULL,
-       NULL,
-       0,
-       STATUS_SUCCESS);
-   /* Ignore the values of pt_Timeout, pu8_Data, pu32_DataLength and
-    * u32_ReadLength */
-   SimpleImager_FSS_readFileFs_IgnoreArg_pt_Timeout();
-   SimpleImager_FSS_readFileFs_IgnoreArg_pu8_Data();
-   SimpleImager_FSS_readFileFs_IgnoreArg_pu32_DataLength();
-   SimpleImager_FSS_readFileFs_IgnoreArg_u32_ReadLength();
-
-   /* Return the malformed data and its length through the right pointers */
-   SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data(
-       &ru8_MalformedData[0], u32_MalformedDataLength);
-   SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength(
-       &u32_MalformedDataLength);
-
-   /* Expect SimpleImager to then close the file */
-   SimpleImager_FSS_closeFileFs_ExpectAndReturn(
-       gpt_SimpleImager, NULL, t_File, STATUS_SUCCESS);
-   SimpleImager_FSS_closeFileFs_IgnoreArg_pt_Timeout();
-
-   t_Status = SimpleImager_loadSettings(
-       gpt_SimpleImager, (const uint8_t *)z_FileName, strlen(z_FileName));
-   TEST_ASSERT_NOT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   /* Call the loadSettings action with malformed data */
+   status_t t_Status = loadSettingsFromString("Malformed data");
+   TEST_ASSERT_NOT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

At this stage it would be wise to build and run the revised SimpleImager_Test_loadSettingsFailureMalformedData() test and make sure nothing is broken.

Build and run the SimpleImager tests again and confirm that SimpleImager_Test_loadSettingsFailureMalformedData() still passes.

Note that if, for some reason, the way in which SimpleImager loaded settings changed, only the loadSettingsFromString() function would need to be updated, and the Unit Test functions themselves would not require any changes.

You can now very easily use loadSettingsFromString() to define a whole host of tests for SimpleImager which would focus on the settings parser.

You’ll do exactly this in the next section.

Test 4: loadSettings Successful

The final test listed above needs to make sure that, when given valid settings data, SimpleImager parses and presents the values correctly. You’ll make use of the loadSettingsFromString() function you just added to do this.

Modify the autogenerated SimpleImager_Test_loadSettingsSuccessfulNoArg() test to implement this new test, since it is not required for this tutorial.

Rename SimpleImager_Test_loadSettingsSuccessfulNoArg() to an appropriate name for the test you are implementing, and update its description accordingly.

Update the body of the function to make use of the loadSettingsFromString() helper function to invoke the loadSettings action with valid settings data.

Recall from the previous tutorial that valid settings are of the form T=0;H=0;W=0;, where T, H and W can be any 16-bit unsigned integer.

Show the updated test.
-/** Test successfully invoking the loadSettings action without an argument */
-void SimpleImager_Test_loadSettingsSuccessfulNoArg(void)
+/** Test that loadSettings succeeds when reading a valid settings file */
+void SimpleImager_Test_loadSettingsSuccessfulValidData(void)
{
-   status_t t_Status;              /* The current status */
-
-   /* TODO: Set up any expected external function calls for this action */
-
-   t_Status = SimpleImager_loadSettings(
-       gpt_SimpleImager, NULL, 0);
+   status_t t_Status = loadSettingsFromString("T=145;H=480;W=640;");
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

Build and run the SimpleImager tests and confirm that SimpleImager_Test_loadSettingsSuccessfulValidData() currently passes.

To complete this test you still need to check that the correct values were parsed by SimpleImager. To do this, you can check the values of the exposureTime, imageHeight and imageWidth Parameters.

Add calls to SimpleImager_getExposureTime(), SimpleImager_getImageHeight(), and SimpleImager_getImageWidth(). Use Unity assertions to check their returned statuses and Parameter values.

Show the Parameter checks added to SimpleImager_Test_loadSettingsSuccessfulValidData().
/** Test that loadSettings succeeds when reading a valid settings file */
void SimpleImager_Test_loadSettingsSuccessfulValidData(void)
{
+   uint16_t u16_ExposureTime, u16_ImageHeight, u16_ImageWidth;

    status_t t_Status = loadSettingsFromString("T=145;H=480;W=640;");
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+
+   t_Status = SimpleImager_getExposureTime(
+       gpt_SimpleImager, &u16_ExposureTime);
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT16(145, u16_ExposureTime);
+
+   t_Status = SimpleImager_getImageHeight(
+       gpt_SimpleImager, &u16_ImageHeight);
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT16(480, u16_ImageHeight);
+
+   t_Status = SimpleImager_getImageWidth(
+       gpt_SimpleImager, &u16_ImageWidth);
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT16(640, u16_ImageWidth);
}

Build and run the SimpleImager tests again and confirm that the completed SimpleImager_Test_loadSettingsSuccessfulValidData() still passes.

This completes the set of tests outlined above.

Notice that some of the remaining autogenerated tests are still failing. This is due to their implementation being incomplete as indicated by their TODO comments. Completing their implementation is not within the scope of this tutorial, however, if you wish to reinforce what you have learned in this tutorial, feel free to give it a go. The solution can be seen below.

Show the updated implementation for the remaining tests.
/** Test successfully getting the SettingsLoaded parameter */
void SimpleImager_Test_getSettingsLoadedSuccessful(void)
{
    status_t t_Status;      /* The return status */
    /* The value to test with */
    bool b_Value = false;

-   /* TODO: Set up any expected external function calls for this parameter */
-
    t_Status = SimpleImager_getSettingsLoaded(
        gpt_SimpleImager, &b_Value);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_FALSE(b_Value);
-
-   /* TODO: test that the retrieved values match what is expected */
}

/** Test successfully getting the ExposureTime parameter */
void SimpleImager_Test_getExposureTimeSuccessful(void)
{
    status_t t_Status;      /* The return status */
    /* The value to test with */
    uint16_t u16_Value = 0;
+   uint16_t u16_ExpectedValue = 145;

-   /* TODO: Set up any expected external function calls for this parameter */
+   t_Status = loadSettingsFromString("T=145;H=480;W=640;");
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);

    t_Status = SimpleImager_getExposureTime(
        gpt_SimpleImager, &u16_Value);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT16(u16_ExpectedValue, u16_Value);
-
-   /* TODO: test that the retrieved values match what is expected */
}

/** Test successfully getting the ImageHeight parameter */
void SimpleImager_Test_getImageHeightSuccessful(void)
{
    status_t t_Status;      /* The return status */
    /* The value to test with */
    uint16_t u16_Value = 0;
+   uint16_t u16_ExpectedValue = 480;

-   /* TODO: Set up any expected external function calls for this parameter */
+   t_Status = loadSettingsFromString("T=145;H=480;W=640;");
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);

    t_Status = SimpleImager_getImageHeight(
        gpt_SimpleImager, &u16_Value);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT16(u16_ExpectedValue, u16_Value);
-
-   /* TODO: test that the retrieved values match what is expected */
}

/** Test successfully getting the ImageWidth parameter */
void SimpleImager_Test_getImageWidthSuccessful(void)
{
    status_t t_Status;      /* The return status */
    /* The value to test with */
    uint16_t u16_Value = 0;
+   uint16_t u16_ExpectedValue = 640;

-   /* TODO: Set up any expected external function calls for this parameter */
+   t_Status = loadSettingsFromString("T=145;H=480;W=640;");
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);

    t_Status = SimpleImager_getImageWidth(
        gpt_SimpleImager, &u16_Value);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT16(u16_ExpectedValue, u16_Value);
-
-   /* TODO: test that the retrieved values match what is expected */
}

Using the techniques you’ve learned in this tutorial you could write more tests to further exercise SimpleImager. Have a think about what these test might be and how you’d write them.

Perhaps also consider how you might write Unit Tests for the MessagePrinter and EventPublisher Component Types you encountered in the first three tutorials.

Wrap Up

In this tutorial you have:

  • Learned about how Unit Testing is supported by Flightkit.

  • Learned how to use Flightkit Tooling to generate, build and run Unit Tests.

  • Implemented and run Unit Tests for the SimpleImager Component Type, making use of mocking to fulfil its FSS requirement.

In the next tutorial, you’ll learn how to use Services to let your Component Types interact with hardware.

Click here to move on to that now.