DEV Community

Aluizio Tomazelli Junior
Aluizio Tomazelli Junior

Posted on

Unit testing ESP-IDF components with GoogleTest (host-based)

I had been looking for a way to test ESP-IDF component logic without flashing to the board every time. The cycle of edit, flash, read serial gets old fast, especially when the bug is just in a calculation or a state machine — nothing hardware-specific.

ESP-IDF supports building for a Linux target, which means you can run tests straight on your machine. I put together a repo to document how I set this up, using GoogleTest as the test framework. This post covers the first example: aluiziotomazelli/gtest-esp-idf.


Prerequisites

To run these tests you need ESP-IDF 5.x installed and sourced, a Linux machine or WSL2, and two system packages that the IDF linux target depends on:

sudo apt install libbsd0 libbsd-dev
Enter fullscreen mode Exit fullscreen mode

If you use the official ESP-IDF Docker container (idf-env latest), these are already included and you can skip this step — the CI badges on the repo run exactly that way, with no extra setup.

Ruby is not needed for this example. It only comes in when using CMock-based IDF mocks, which is a different approach covered in a future chapter.


Why GoogleTest instead of Unity?

Unity is a native ESP-IDF component — it's already there, no setup needed. For simple assertions it works fine. The issue shows up when your component has multiple C++ classes with distinct responsibilities and you need to test them in isolation.

Unity works alongside CMock, which ESP-IDF uses internally to mock its own components. The problem is that CMock doesn't handle C++ classes, so you end up writing mocks by hand. GMock handles this directly in the test file, in a few lines.


Project layout

The repo is organized as numbered chapters. The first one, 01_basic_test, has this structure:

01_basic_test/
├── CMakeLists.txt
├── include/
│   ├── i_sum.hpp     # Interface (abstract base class)
│   └── sum.hpp       # Concrete class header
├── src/
│   └── sum.cpp       # Production code
├── host_test/
│   ├── gtest/        # GTest wrapper component
│   └── test_sum/     # Test project for the Sum class
└── test_apps/        # Hardware verification
Enter fullscreen mode Exit fullscreen mode

Production code stays in src/ and include/. Tests stay in host_test/. They don't mix.

Why the interface?

i_sum.hpp defines an abstract base class for Sum. For this example it's not strictly needed — the class is simple enough to test directly. But it's worth doing from the start: in future chapters, we'll derive mocks from interfaces using GMock, which lets you test components that depend on a class without using the real implementation. Without an interface, GMock can't create a mock.


The GTest wrapper

GoogleTest isn't part of ESP-IDF, so it needs to be introduced as a component. The wrapper lives at host_test/gtest/ and is just a CMakeLists.txt — no source files. A few things worth explaining:

Linux-only guard. GTest only gets processed when the build target is linux. It never ends up in the ESP32 binary.

if(IDF_TARGET STREQUAL "linux")
Enter fullscreen mode Exit fullscreen mode

FetchContent. GTest is downloaded from GitHub at build time. The repo stays small, and updating the version is just changing the tag.

FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        v1.14.0
)
Enter fullscreen mode Exit fullscreen mode

Build phase guard. ESP-IDF runs in two phases: first it scans components to resolve dependencies, then it builds. FetchContent can't run during the scan — it would fail trying to download something while the build system is still mapping dependencies. The guard keeps it to the real build phase.

if(NOT CMAKE_BUILD_EARLY_EXPANSION)
  FetchContent_MakeAvailable(googletest)
endif()
Enter fullscreen mode Exit fullscreen mode

INTERFACE linking. Since the wrapper has no source files, it registers as an INTERFACE component — it doesn't compile anything itself, just exposes GTest and GMock to whoever lists gtest in their REQUIRES. This pattern keeps the wrapper reusable as more test projects are added.


Test project configuration

host_test/test_sum/ is a standalone ESP-IDF project. Two CMake details matter here:

EXTRA_COMPONENT_DIRS — the component under test and the GTest wrapper are outside this project's folder. This tells the build system where to find them.

COMPONENTS — without this, the build would process the entire ESP-IDF component tree. Listing only what's needed cuts compilation time significantly.

WHOLE_ARCHIVE — GoogleTest registers tests through static constructors. Since the test functions are never called directly by main.cpp, the linker may discard them as unused. WHOLE_ARCHIVE forces every object file to be included, so all tests are actually discovered and run. Without this, you can have a successful build with zero tests executing.


Test structure

The test project is at host_test/test_sum/. The Sum class has two methods: add(a, b) for plain addition, and add_constrained(a, b) which returns -1 if the result exceeds the allowed range. Three test groups:

Smoke test — checks that GoogleTest itself is running. If this fails, the problem is the setup, not the code.

Standard addition — tests add(a, b) with positives, negatives, and zero.

Constrained addition — three scenarios: inputs within range, at the exact limit, and over it.

TEST(TestSum, AddConstrained_OutOfRange) {
    Sum s;
    EXPECT_EQ(s.add_constrained(6, 5), -1);  // 11 > 10, should return -1
}
Enter fullscreen mode Exit fullscreen mode

Running the tests

cd 01_basic_test/host_test/test_sum
idf.py --preview set-target linux
idf.py build
./build/test_sum.elf
Enter fullscreen mode Exit fullscreen mode

The --preview flag is needed because the Linux target is still marked as experimental in ESP-IDF. It switches the compiler from Xtensa/RISC-V to your local GCC.

Expected output:

[==========] Running 6 tests from 1 test suite.
[----------] 6 tests from TestSum
[ RUN      ] TestSum.GTestSmokeTest
[       OK ] TestSum.GTestSmokeTest (0 ms)
...
[  PASSED  ] 6 tests.
Enter fullscreen mode Exit fullscreen mode

CI

The repo has GitHub Actions workflows that run the host tests on every push using the official ESP-IDF container. Since no hardware is needed, this works out of the box with no self-hosted runners.
Build Status Host Tests Status


What's next

The next chapters will cover mocks — both GMock for testing components in isolation, and IDF's CMock-based approach for mocking hardware dependencies. Full source at github.com/aluiziotomazelli/gtest-esp-idf

Top comments (0)