Essential Unit Testing Frameworks for Embedded Software Development

Follow+Star Public Account Number, don’t miss wonderful content
Essential Unit Testing Frameworks for Embedded Software Development
Source | Big Orange Crazy Embedded
As a qualified embedded software engineer, it is essential to write not only business code but also unit test code. At this time, having a set of unit testing frameworks becomes particularly important.

In software development, each change in requirements basically requires rewriting code, and after code changes, functional testing must be performed.

Of course, before functional testing, unit testing of the code is required to avoid unverified scenarios after code modifications, which can lead to various issues.

Quickly completing unit tests through testing frameworks not only covers previously tested scenarios but also quickly identifies where the problems are.

Common C language testing frameworks include:

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

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

  • Check: A unit testing framework for C language, easy to use, supports test suite and test case management, convenient for maintaining test components.

  • Google Test: A C++ testing framework launched by Google, supports C language, can be cross-platform, and has a rich assertion library and mocks.

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

  • 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 checked by interested parties, as different unit testing frameworks suit 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; 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

Complete verification of functional functions:

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


void setUp() {
    // Initialization code before each test case runs can be placed here
}

void tearDown() {
    // Cleanup code after each test case runs can be placed here
}

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

Among them, the unity_internals.h file can modify the output terminal, that is, 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

Among them, the container functions of the custom C language extension library (cot) have all been added with corresponding unit test cases 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 various container implementations, including general queues (including variable-length queues), stacks, doubly linked lists, and dynamic array functions.

    Doubly linked list nodes can be dynamically created (need to allocate memory 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 structure functions.

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

  3. Ported some functions from the C++ Boost library’s PP library.

    Achieves complex macro languages through macro syntax, flexibly used, generating the desired code at compile time.

2. Software Architecture

Directory description:

├─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) Container Class Function Usage Instructions

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 elements to the head
    cotList_PushFront(&list, &data1);

    // Add elements to the tail
    cotList_PushBack(&list, &data2);

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

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

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

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

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

    // Swap memory of linked 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 tail
    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 addition
    cotVector_RemoveIf(&vector, OnVectorRemoveCondition);

    // Print the data content 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 tail
    int data = 42;
    cotQueue_Push(&queue, &data, sizeof(data));
    data = 895;
    cotQueue_Push(&queue, &data, sizeof(data));

    // Access elements
    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 tail
    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 elements
    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 elements
    int *pData = (int *)cotStack_Top(&stack);
    printf("val = %d \n", *pData);

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

    return 0;
}

(2) Serialization/Deserialization Function Usage Instructions

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
Each module references header files:
#include "struct.h"

int main()
{
    uint8_t buf[100];

    // Serialization usage 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 usage 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;
}
Disclaimer:This article’s material comes from the internet, and the copyright belongs to the original author. If there are copyright issues, please contact me for removal.
———— END ————
Essential Unit Testing Frameworks for Embedded Software Development
● Column “Embedded Tools
● Column “Embedded Development”
● Column “Keil Tutorial”
● Selected Tutorials for Embedded Columns
Follow the public account reply “Join Group” to join the technical exchange group as per the rules, reply “1024” to see more content.
Essential Unit Testing Frameworks for Embedded Software Development
Essential Unit Testing Frameworks for Embedded Software Development
Click “Read the Original” to see more shares.

Leave a Comment

×