The Engineering Value of Embedded Unit Testing and an Overview of the Unity Framework

This is not a “slogan” article, but rather a response to three very practical questions from the front line of embedded projects:1) Is it worth doing unit testing in embedded projects?2) How to implement it under tight resources and strong hardware dependencies?3) Why is Unity a “practically feasible” choice?

1. Let’s be honest: What are embedded projects really afraid of?

After working on several mass production projects, most embedded engineers will have similar feelings:

  • • Not enough boards: Ten people fighting over two prototypes, even serial cables have to wait in line.
  • • Changing a line of code requires board verification: Even if it’s just an if condition, it doesn’t feel secure.
  • • Testing relies entirely on manual effort and habits: “It seems like we tested it this way before,” but no one dares to say, “It has all been tested.”
  • • Bugs always appear on the customer site: Everything is “normal” in the local environment, but it goes out of control as soon as it powers up on site.

From an engineering perspective, this is essentially due tolong feedback cycles:

The Engineering Value of Embedded Unit Testing and an Overview of the Unity Framework

What unit testing aims to do is actually very simple:to bring the time of “problem discovery” as close as possible to “the moment of writing code” from integration/on-site..

The Engineering Value of Embedded Unit Testing and an Overview of the Unity Framework

This is often referred to as “shifting defects left,” but in the embedded context, it has a more straightforward meaning:

If a problem can be discovered in front of a computer, don’t let it linger on the board or on-site.

2. The three major obstacles to embedded unit testing

Unlike web/backend, embedded unit testing faces three very real resistances.

2.1 Resource constraints: ROM/RAM are “precious commodities”

  • • MCU RAM may only be a few KB to several tens of KB.
  • • ROM often only has a few hundred KB, or even less.
  • • Some projects don’t even dare to casually enable printf.

Therefore, the most basic requirement for a testing framework is:

Lightweight, customizable, and does not compete for resources with business code.

This directly rules out many “large and comprehensive” testing frameworks (like gtest).

2.2 Strong hardware dependency: Cannot “breathe” without the board

Typical embedded code often looks like this:

// Directly read/write registers/peripherals
uint16_t read_temperature_raw(void) {
    return ADC1->DR;
}

float get_temperature_celsius(void) {
    uint16_t raw = read_temperature_raw();
    return raw * 0.0625f;
}

The problem is:

  • • This code cannot run on a PC without the MCU registers.
  • • Business logic and hardware details are mixed together, making it difficult to test and maintain.

To make unit testing truly work,it is essential to introduce “replaceable hardware interfaces”, commonly referred to as HAL/Mock, which will be elaborated in subsequent articles.

2.3 Engineering portability: Testing cannot be “tied down” to a specific IDE

  • • Many teams rely on IDE project files (Keil/IAR) for builds, and switching platforms can be very painful.
  • • If testing is tied to a specific IDE’s “Run Test” button, future migration will be very troublesome.

A more reliable approach is:

project_root/
├─ src/        # Only product code
├─ tests/      # Only test code
├─ unity/      # Unity framework source code
└─ CMake/Make  # Build scripts (portable)

Product code can be compiled independently,test code can also be compiled independently, with both only interacting through header files and interfaces.

3. Why Unity is suitable for embedded: An expert’s perspective on “trade-offs”

Those involved in embedded systems usually do not lack the ability to “reinvent the wheel,” but when it comes to testing frameworks, it is more advisable toprioritize mature tools, focusing only on integration and engineering practices.

First, let’s look at a simple comparison table:

Framework Language/Positioning Dependency Situation Realistic Evaluation in Embedded
GoogleTest C++, General Requires C++/STL Powerful, but too heavy for small MCUs
Check C, for Unix-like Depends on libc, etc. Suitable for Linux user space, not for bare metal
CUnit C, Lightweight Fewer dependencies More suitable for small PC projects
Unity Pure C, Embedded No external dependencies Can run on almost all MCUs

From the perspective of an embedded development expert,several key points of Unity are very “engineering-friendly”:

  1. 1. Pure C implementation + few files
  • • Essentially just <span>unity.c</span> + <span>unity.h</span>, which can be used as soon as it is included in the project.
  • 2. Almost zero dependencies
    • • Does not depend on dynamic libraries, RTOS, or specific platform APIs.
  • 3. Sufficient variety of assertions
    • • Supports common assertions for integers, floats, pointers, strings, arrays, memory blocks, etc.
  • 4. Complete ecosystem but not tightly coupled
    • Can be used independently or combined with CMock and Ceedling to form a complete toolchain.

    A simplified architecture diagram can illustrate its position in a project:

    The Engineering Value of Embedded Unit Testing and an Overview of the Unity Framework

    4. A small practical example: How is the CRC function “protected by testing”?

    4.1 Without testing: CRC is a “black box that seems fine based on experience”

    Typically, CRC16 (Modbus) would be written like this:

    uint16_t crc16_modbus(const uint8_t* data, size_t len) {
        uint16_t crc = 0xFFFF;
        for (size_t i = 0; i < len; i++) {
            crc ^= data[i];
            for (int j = 0; j < 8; j++) {
                if (crc & 0x0001) {
                    crc = (crc >> 1) ^ 0xA001;
                } else {
                    crc >>= 1;
                }
            }
        }
        return crc;
    }

    It looks fine at first glance, but:

    • • You don’t know if it meets the standard test vectors in the protocol specification.
    • • You also cannot prevent a bug from being inadvertently introduced during a subsequent “optimization”.

    4.2 After Unity: CRC becomes a verifiable and provable module

    With Unity, you can write test cases like this (illustrative):

    #include "unity.h"
    #include "crc16.h"   // Declaration of crc16_modbus
    
    void setUp(void) {}
    void tearDown(void) {}
    
    void test_crc16_modbus_standard_frame(void) {
        uint8_t frame[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01};
        uint16_t crc = crc16_modbus(frame, sizeof(frame));
        TEST_ASSERT_EQUAL_HEX16(0x840A, crc);  // From Modbus specification
    }
    
    void test_crc16_modbus_empty_data(void) {
        uint16_t crc = crc16_modbus(NULL, 0);
        TEST_ASSERT_EQUAL_HEX16(0xFFFF, crc);  // Initial value
    }
    
    int main(void) {
        UNITY_BEGIN();
        RUN_TEST(test_crc16_modbus_standard_frame);
        RUN_TEST(test_crc16_modbus_empty_data);
        return UNITY_END();
    }

    The direct benefits in engineering are:

    • Algorithm verifiable: Backed by standard vectors, it is no longer based on “feeling”.
    • Modifications are regressable: Any changes to <span>crc16_modbus</span> can be reverted with one click.
    • Issues are traceable: It is easy to see which input led to an erroneous output.

    Moreover, this module does not depend on hardware registers, allowing it to run on a PC first and then be ported to the target board as needed to verify performance.

    5. From “want to do” to “can do”: The minimal path to implementing embedded unit testing

    Many teams fail in unit testing not because they don’t know how to use the framework, but becausethey try to do everything at once and end up not sticking to anything.

    The following path has been validated in multiple projects and is relatively low-resistance:

    The Engineering Value of Embedded Unit Testing and an Overview of the Unity Framework

    5.1 Step one: Start with the “easiest module” rather than the “most important module”

    • • Don’t try to test the entire set of drivers/protocols right from the start.
    • • You can start with the following types of modules:
      • • CRC/check algorithms
      • • Digital filtering, PID control
      • • Frame parsing, state machines
      • • Simple business rules (like alarm determination)

    These modules have several common points:

    • Almost no hardware dependencies, suitable for running tests on a PC;
    • Errors affect the entire system, making testing benefits obvious;
    • Code is relatively independent, making refactoring costs manageable.

    5.2 Step two: Leave a “legal position” for testing in the project structure

    At the very least, ensure:

    project_root/
    ├─ src/        # Product code
    │  ├─ crc16.c
    │  ├─ filter.c
    │  └─ ...
    ├─ include/    # Header files
    ├─ tests/      # Test code
    │  ├─ test_crc16.c
    │  ├─ test_filter.c
    │  └─ ...
    └─ unity/      # unity.c / unity.h

    Key point:Do not put test code in src/, otherwise subsequent CI/build/code analysis will be very painful.

    5.3 Step three: Get it running in a way you are familiar with

    • • If you are used to CMake, just add an <span>add_executable(test_crc16 ...)</span> in CMake;
    • • If you are used to Makefile, write a minimal <span>make test</span> target;
    • • Even in an IDE (Keil/IAR), you can create a “test project” that only compiles the algorithm + Unity.

    The goal is not to create an “enterprise-level CI” all at once, but to achieve:

    “I modified the CRC function, and I can run the tests within 5 seconds to know if I messed up.”

    6. Engineering trade-offs of Unity compared to other solutions

    Let’s simplify the comparison (from an engineering/implementation perspective, not a functional perspective):

    Solution Engineering Integration Cost Pressure on MCU Resources Mocking Capability Suitable Stage
    Handwritten assertions Low, but chaotic and unstructured Very low Need to reinvent the wheel Individual practice, small scripts
    CUnit Medium, requires additional dependencies Medium Weak PC-side C projects
    Check Medium to high, geared towards Linux users Heavy for small MCUs Average Gateways/upper computers running Linux
    GoogleTest High (C++/STL) Too heavy for embedded Very strong C++/Linux server-side
    Unity Low, just copy two files Controllable, customizable Very strong with CMock Typical MCU/RTOS/bare metal projects

    Recommending Unity is not because it is the “strongest,” but because it strikes a balance betweenfunctionality, complexity, and integration cost that aligns very well with the realities of embedded systems.

    7. Conclusion: From “Should we test?” to “Where to start testing and how to test”

    Three practical suggestions:

    1. 1. Stop worrying about “whether to do unit testing” and instead decide “where to start”
    • • Choose one or two pure algorithm or protocol modules and get them running on PC with Unity;
    • • Once you experience the convenience of regression, expand the scope, which is much more effective than trying to do everything at once.
  • 2. Consider “testability” as a dimension of code quality
    • • If a piece of code “cannot be tested at all,” it often indicates too many responsibilities and heavy coupling;
    • • Moderately refactoring interfaces for testing is itself an investment in future maintenance.
  • 3. Unity is just a tool; the key is team habits
    • • Engineering benefits come from having a set of quick tests that can run before and after every change;
    • • Once you get used to “running tests before submitting,” it becomes hard to revert to relying solely on manual regression.

    Leave a Comment