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

Published: (February 22, 2026 at 01:05 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Introduction

I wanted a way to test ESP‑IDF component logic without flashing the board each time.
ESP‑IDF can build for a Linux target, allowing tests to run directly on the host machine.
This guide shows how to set that up using GoogleTest as the test framework. The example repository is aluiziotomazelli/gtest‑esp‑idf (chapter 01_basic_test).

Prerequisites

  • ESP‑IDF 5.x installed and sourced
  • Linux machine or WSL2
  • System packages required by the ESP‑IDF Linux target
sudo apt install libbsd0 libbsd-dev

If you use the official ESP‑IDF Docker container (idf-env:latest), the packages are already present.

Note: Ruby is only needed for CMock‑based IDF mocks, which are covered in later chapters.

Repository layout

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 (CMake only)
│   └── test_sum/    # Test project for the Sum class
└── test_apps/        # Hardware verification (not used for host tests)
  • src/ and include/ contain production code.
  • host_test/ contains all host‑side test code; the two directories never mix.

i_sum.hpp

Defines an abstract base class for Sum. It isn’t strictly required for this simple example, but it illustrates a pattern that will be useful when mocking with GMock later.

Adding GoogleTest as a component

GoogleTest isn’t part of ESP‑IDF, so we introduce it via a wrapper component located at host_test/gtest/. The wrapper consists only of a CMakeLists.txt file:

# host_test/gtest/CMakeLists.txt
if(IDF_TARGET STREQUAL "linux")
    include(FetchContent)

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

    # FetchContent must run during the real build phase, not the dependency‑scan phase
    if(NOT CMAKE_BUILD_EARLY_EXPANSION)
        FetchContent_MakeAvailable(googletest)
    endif()

    # Register as an INTERFACE library so other components can link to GTest/GMock
    add_library(gtest INTERFACE)
    target_link_libraries(gtest INTERFACE gtest gmock)
    target_include_directories(gtest INTERFACE
        ${googletest_SOURCE_DIR}/include
    )
endif()

Key points

  • Linux‑only guard – the component is processed only when the build target is linux.
  • FetchContent – downloads GTest at build time; updating the version only requires changing GIT_TAG.
  • Build‑phase guard – prevents download attempts during the early dependency‑scan phase.
  • INTERFACE library – the wrapper itself compiles nothing; it merely exposes GTest/GMock to projects that REQUIRES it.

Test project configuration

The test project lives in host_test/test_sum/. Important CMake settings:

# host_test/test_sum/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)

set(EXTRA_COMPONENT_DIRS
    "${CMAKE_CURRENT_LIST_DIR}/../../.."   # root of the repo (contains the component under test)
    "${CMAKE_CURRENT_LIST_DIR}/../gtest"   # GTest wrapper
)

idf_component_register(
    SRCS "test_sum.cpp"
    INCLUDE_DIRS "."
    REQUIRES sum gtest
    WHOLE_ARCHIVE   # Ensure static constructors (GoogleTest registration) are not discarded
)
  • EXTRA_COMPONENT_DIRS points to the component under test (sum) and the GTest wrapper, which are outside the test project folder.
  • REQUIRES lists only the needed components, keeping the build fast.
  • WHOLE_ARCHIVE forces inclusion of all object files so that GoogleTest can discover the tests.

Example test code

// host_test/test_sum/test_sum.cpp
#include 
#include "sum.hpp"

TEST(TestSum, GTestSmokeTest) {
    EXPECT_TRUE(true);   // verifies that GoogleTest itself runs
}

TEST(TestSum, Add) {
    Sum s;
    EXPECT_EQ(s.add(2, 3), 5);
    EXPECT_EQ(s.add(-1, -4), -5);
    EXPECT_EQ(s.add(0, 0), 0);
}

TEST(TestSum, AddConstrained_InRange) {
    Sum s;
    EXPECT_EQ(s.add_constrained(3, 4), 7);   // 7 ≤ 10
}

TEST(TestSum, AddConstrained_AtLimit) {
    Sum s;
    EXPECT_EQ(s.add_constrained(5, 5), 10);  // exactly the limit
}

TEST(TestSum, AddConstrained_OutOfRange) {
    Sum s;
    EXPECT_EQ(s.add_constrained(6, 5), -1); // 11 > 10 → -1
}

Building and running the host tests

cd 01_basic_test/host_test/test_sum
idf.py --preview set-target linux   # Linux target is still experimental
idf.py build
./build/test_sum.elf

Expected output

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

Continuous Integration

The repository includes GitHub Actions workflows that execute the host tests on every push using the official ESP‑IDF Docker container. No hardware is required, so the CI runs on standard GitHub runners.

What’s next?

Future chapters will cover:

  • GMock for isolating components via mock objects.
  • CMock‑based approach (used internally by ESP‑IDF) for mocking hardware dependencies.

Full source code is available at github.com/aluiziotomazelli/gtest-esp-idf.

0 views
Back to Blog

Related posts

Read more »