How to Complete Unit Testing for Embedded Code?

Follow+Star Public Account Number, don’t miss out on exciting content

Source | Big Orange Crazy Embedded

In software development, every change in requirements generally necessitates rewriting code. After code changes, functional testing is required, and of course, unit testing must be performed before functional testing to avoid unverified scenarios after code modifications, which can lead to various issues.

Quickly completing unit tests through a testing framework not only covers previously tested scenarios but also quickly reflects 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. It is simple to use and commonly used in embedded system development.

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

  • Check: A unit testing framework suitable for C language, easy to use, supports management of test suites and test cases, making it easy to maintain testing components.

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

  • cmocka: A unit testing framework suitable for C language that supports memory leak detection and advanced usage such as mock functions and stub functions.

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

Unity Example

This section introduces Unity; others can be researched independently as different unit testing frameworks are suitable for 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 following three files from the Unity source directory:<span><span>unity.c</span></span>, <span><span>unity.h</span></span>, and <span><span>unity_internals.h</span></span> to our project directory for compilation, and then include <span><span>unity.h</span></span> in the test file code.

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

A simple example to validate the functional 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 <span><span>unity_internals.h</span></span> file can modify the output terminal, i.e., the definition of the <span><span>UNITY_OUTPUT_CHAR</span></span> 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

All container function units of the custom C language extension library (cot) have corresponding unit test cases added through Unity, link:

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

Lightweight General Extension Library

Aimed at creating a general extension library for the C language.

Introduction

  1. Supports multiple 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 (memory needs to be allocated during initialization) or statically added. Dynamic arrays maximize the use of memory allocated during initialization and support random access (contiguous addresses).

  2. Supports defining serialization/deserialization structures.

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

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

    Achieves complex macro language through macro syntax, allowing flexible usage and generating desired code at compile time.

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

Usage Instructions

Usage instructions for container class functions

Doubly linked list usage demo

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

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

// Add elements to the head
    cotList_PushFront(&amp;list, &amp;data1);

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

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

// Use iterator to traverse all elements
    for_list_each(item, list)
    {
        printf(" = %d\n", *item_ptr(int, item));
    }

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

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

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

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

return 0;
}

Dynamically array usage demo

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

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

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

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

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

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

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

return 0;
}

Doubly queue (fixed-length FIFO) usage demo

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

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

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

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

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

return 0;
}

Queue (variable-length FIFO) usage demo

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

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

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

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

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

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

return 0;
}

Single stack usage demo

int main()
{
uint8_t buf[10];
    cotStack_t stack;

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

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

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

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

return 0;
}

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 for usage

#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, &amp;test);

    printf("Serialize: \n");

for (int i = 0; i &lt; 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(&amp;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 any copyright issues, please contact me for deletion.

———— END ————

How to Complete Unit Testing for Embedded Code?

● Column “Embedded Tools”

● Column “Embedded Development”

● Column “Keil Tutorial”

● Selected Tutorials from the Embedded Column

Follow the public account Reply “Join Group” to join the technical exchange group according to the rules, reply “1024” to see more content.

Click “Read the original text” to see more shares.

Leave a Comment