How to Complete Unit Testing for Embedded Code?

In software development, every change in requirements generally requires rewriting code, and after code changes, functionality testing is needed. Of course, before functionality testing, unit testing of the code is necessary to avoid unverified scenarios after code modifications, which can lead to various issues.

Using a testing framework to quickly complete unit testing of the code can not only cover previously tested scenarios but also quickly identify where the problems are.

Common C language testing frameworks include:

  • Unity: A small, open-source C language testing framework that provides basic structures and functions for testing. Simple to use and commonly used in embedded system development.

  • CUnit: A framework for testing C language, easy to use, supports automated and manual testing.

  • Check: A unit testing framework for C language, easy to use, supports management of test suites and test cases, facilitating maintenance of test components.

  • Google Test: A C++ testing framework launched by Google, which supports C language and is cross-platform, with a rich library of assertions and mocks.

  • cmocka: A unit testing framework for C language, supports memory leak detection, mock functions, and stub functions, among other advanced usages.

  • criterion: A unit testing framework based on C language, supports parameterized testing and test case dependencies, with good performance and ease of use.

1. Unity Example

This section introduces Unity; others can be looked up independently as different unit testing frameworks cater to different development needs and scenarios. Developers can choose the most suitable framework according to their project requirements.

Unity can be completed with just a few files. Simply copy the three files unity.c, unity.h, and unity_internals.h from the Unity source directory to our project directory for compilation, and then include unity.h in the test file code.

https://github.com/ThrowTheSwitch/Unity/releases

Simple Example

Verifying the functionality function:

#include <stdio.h>
#include "unity.h"


void setUp() {
    // Initialization code before each test case runs
}

void tearDown() {
    // Cleanup code after each test case runs
}

int Add(int a, int b)
{
    return a + b;
}

void test_AddFun(void)
{
    TEST_ASSERT_EQUAL_UINT(6, Add(1, 5));
    TEST_ASSERT_EQUAL_UINT(4, Add(-1, 5));
    TEST_ASSERT_EQUAL_UINT(-6, Add(-1, -5));
}


int main()
{
    UNITY_BEGIN();  // Start testing

    RUN_TEST(test_AddFun);
    UNITY_END();  // End testing

    return 0;
}  

The output printed via serial port or terminal is:

C:\test/test.c:47:test_AddFun:PASS

-----------------------
1 Tests 0 Failures 0 Ignored
OK

In which, the unity_internals.h file can modify the output terminal, i.e., the definition of the UNITY_OUTPUT_CHAR macro.

/*-------------------------------------------------------
 * Output Method: stdout (DEFAULT)
 *-------------------------------------------------------*/
#ifndef UNITY_OUTPUT_CHAR
  /* Default to using putchar, which is defined in stdio.h */
  #include <stdio.h>
  #define UNITY_OUTPUT_CHAR(a) (void)putchar(a)
#else
  /* If defined as something else, make sure we declare it here so it's ready for use */
  #ifdef UNITY_OUTPUT_CHAR_HEADER_DECLARATION
    extern void UNITY_OUTPUT_CHAR_HEADER_DECLARATION;
  #endif
#endif

In addition, the container functions of the custom C language extension library (cot) have corresponding unit test cases added via Unity. Link:

https://gitee.com/const-zpc/cot

2. Lightweight General Extension Library

Aimed at creating a general extension library for C language.

1. Introduction

  1. Supports multiple container implementations, including general queues (including variable-length queues), stacks, doubly linked lists, and dynamic arrays.

    Doubly linked list nodes can be dynamically created (requiring memory allocation during initialization) or statically added. Dynamic arrays maximize the use of memory allocated during initialization and support random access (continuous addresses).

  2. Supports defining serialization/deserialization structures.

    Utilizes macro syntax from the PP library in the Boost library; ensures that the structure definitions in header files must remain consistent on both sides.

  3. Some functionalities from the C++ Boost library’s PP library have been ported.

    Complex macro languages are implemented through macro syntax, allowing flexible usage and generating desired code at compile time.

2. Software Architecture

Directory explanation:

├─cot
│  ├─include
│  │  ├─container     // Container implementation header files
│  │  ├─preprocessor  // Ported Boost library's PP library header files
│  │  └─serialize     // Serialization/deserialization implementation header files
│  └─src
│      ├─container    // Container implementation source files
│      └─serialize    // Serialization/deserialization implementation source files
├─test
│  ├─container        // Container implementation test code
│  └─serialize        // Serialization/deserialization test code
└─unity               // Unit testing framework code

3. Usage Instructions

(1) Instructions for Using Container Class Functions

Doubly linked list usage demo:

int main()
{
    cotList_t list;
    cotListItem_t nodeBuf[10];
    cotList_Init(&list, nodeBuf, 10);

    int data1 = 10;
    int data2 = 20;
    int data3 = 30;

    // Add element to the front
    cotList_PushFront(&list, &data1);

    // Add element to the back
    cotList_PushBack(&list, &data2);

    // Insert element
    cotList_Insert(&list, cotList_End(&list), &data3);

    // Iterate through all elements using an iterator
    for_list_each(item, list)
    {
        printf(" = %d\n", *item_ptr(int, item));
    }

    // Remove specified element
    cotList_Remove(&list, &data3);

    // Remove elements based on conditions
    cotList_RemoveIf(&list, OnRemoveCondition);

    cotList_t list2;
    cotListItem_t nodeBuf2[3];
    cotList_Init(&list2, nodeBuf2, 3);

    // Swap memory of lists
    cotList_Swap(&list1, &list2);

    return 0;
}

Dynamic array usage demo:

int main()
{
    uint8_t buf[20];
    cotVector_t vector;

    cotVector_Init(&vector, buf, sizeof(buf), sizeof(uint32_t));

    // Append elements to the back
    uint32_t data = 42;
    cotVector_Push(&vector, &data);
    data = 56;
    cotVector_Push(&vector, &data);
    data = 984;
    cotVector_Push(&vector, &data);

    // Insert elements
    uint32_t arrdata[2] = {125, 656};
    cotVector_InsertN(&vector, 2, &arrdata, 2);

    // Remove two elements
    cotVector_RemoveN(&vector, 1, 2);

    // Remove elements based on conditions
    cotVector_RemoveIf(&vector, OnVectorRemoveCondition);

    // Print data contents in the array
    for (int i = 0; i < cotVector_Size(&vector); i++)
    {
        printf("%02x ", cotVector_Data(&vector)[i]);
    }

    return 0;
}
Doubly queue (fixed-length FIFO) usage demo:
int main()
{
    uint8_t buf[10];
    cotQueue_t queue;

    cotQueue_Init(&queue, buf, sizeof(buf), sizeof(int));

    // Append elements to the back
    int data = 42;
    cotQueue_Push(&queue, &data, sizeof(data));
    data = 895;
    cotQueue_Push(&queue, &data, sizeof(data));

    // Access element
    int *pData = (int *)cotQueue_Front(&queue);
    printf("val = %d \n", *pData);

    // Pop the first element
    cotQueue_Pop(&queue);

    return 0;
}

Queue (variable-length FIFO) usage demo:

int main()
{
    uint8_t buf[10];
    cotIndQueue_t queue;

    cotIndQueue_Init(&queue, buf, sizeof(buf));

    // Append elements to the back
    char data = 42;
    cotIndQueue_Push(&queue, &data, sizeof(data));
    int data1 = 80;
    cotIndQueue_Push(&queue, &data, sizeof(data1));
    long data2 = -400;
    cotIndQueue_Push(&queue, &data, sizeof(data2));

    // Access element
    size_t length;
    int *pData = (int *)cotIndQueue_Front(&queue, &length);

    printf("val = %d \n", *pData, length);

    // Pop the first element
    cotIndQueue_Pop(&queue);

    return 0;
}
Single stack usage demo:
int main()
{
    uint8_t buf[10];
    cotStack_t stack;

    cotStack_Init(&stack, buf, sizeof(buf), sizeof(int));

    // Append elements to the top
    int data = 42;
    cotStack_Push(&stack, &data, sizeof(data));
    data = 895;
    cotQueue_Push(&stack, &data, sizeof(data));

    // Access element
    int *pData = (int *)cotStack_Top(&stack);
    printf("val = %d \n", *pData);

    // Pop the top element
    cotStack_Pop(&stack);

    return 0;
}

(2) Instructions for Serialization/Deserialization Functions

A common header file can be defined:

#ifndef STRUCT_H
#define STRUCT_H

#include "serialize/serialize.h"

COT_DEFINE_STRUCT_TYPE(test_t,
    ((UINT16_T)     (val1)      (2))
    ((INT32_T)      (val2)      (1)) 
    ((UINT8_T)      (val3)      (1))
    ((INT16_T)      (val4)      (1))
    ((DOUBLE_T)     (val5)      (1)) 
    ((INT16_T)      (val6)      (1))
    ((STRING_T)     (szName)    (100))
    ((DOUBLE_T)     (val7)      (1)) 
    ((FLOAT_T)      (val8)      (1))
    ((STRING_T)     (szName1)   (100))
)

#endif // STRUCT_H
Usage of header files in various modules:

#include "struct.h"

int main()
{
    uint8_t buf[100];

    // Serialization demo
    COT_DEFINE_STRUCT_VARIABLE(test_t, test);

    test.val1[0] = 5;
    test.val1[1] = 89;
    test.val2 = -9;
    test.val3 = 60;
    test.val4 = -999;
    test.val5 = 5.6;
    test.val6 = 200;
    test.val7 = -990.35145;
    test.val8 = -80.699;
    sprintf(test.szName, "test56sgdgdfgdfgdf");
    sprintf(test.szName1, "sdfsdf");

    int length = test.Serialize(buf, &test);

    printf("Serialize: \n");

    for (int i = 0; i < length; i++)
    {
        printf("%02x %s", buf[i], (i + 1) % 16 == 0 ? "\n" : "");
    }

    printf("\n");


    // Deserialization demo
    test_t test2;           // COT_DEFINE_STRUCT_VARIABLE(test_t, test2);
    COT_INIT_STRUCT_VARIABLE(test_t, test2);

    test2.Parse(&test2, buf);

    printf("val = %d\n", test2.val1[0]);
    printf("val = %d\n", test2.val1[1]);
    printf("val = %d\n", test2.val2);
    printf("val = %d\n", test2.val3);
    printf("val = %d\n", test2.val4);
    printf("val = %lf\n", test2.val5);
    printf("val = %d\n", test2.val6);
    printf("name = %s\n", test2.szName);
    printf("val = %lf\n", test2.val7);
    printf("val = %f\n", test2.val8);
    printf("name = %s\n", test2.szName1);

    return 0;
}
How to Complete Unit Testing for Embedded Code?

END

Source: Da Chengzi Crazy Embedded
Copyright belongs to the original author. If there is any infringement, please contact to delete.
Recommended Reading
Is CAN Bus Harder Than UART Serial?
Sharing an Open Source Serial Tool, It’s Amazing!
In just three months, Zhi Hui Jun’s entrepreneurial project has received three rounds of financing.

→ Follow to Stay Updated ←

Leave a Comment