Unit Testing Guide

Write and run unit tests for OpenBMC services using GTest/GMock.

Table of Contents

  1. Overview
    1. Why Unit Test?
  2. GTest/GMock Setup with Meson
    1. Meson Configuration
    2. Project Structure
    3. Test Main Entry Point
  3. Writing GTest Tests
    1. Basic Test Structure
    2. Test Fixtures (TEST_F)
    3. Common Assertions
  4. GMock Basics
    1. Creating Mock Classes
    2. Using Mock Objects
    3. Common GMock Matchers
  5. D-Bus Mocking with sdbus++
    1. Why Mock D-Bus?
    2. sdbus++ Mock Utilities
    3. Example: Mocking D-Bus Method Calls
    4. Mocking Property Gets/Sets
    5. Verifying D-Bus Interactions
  6. Running Tests
    1. Via Meson (Local Build)
    2. Via Bitbake
    3. With SDK (Local Development)
    4. Interpreting Test Output
  7. Test Coverage
    1. Enabling Coverage in Meson
    2. Generating Coverage Reports
    3. Coverage Report Interpretation
  8. Practical Example: phosphor-logging
    1. Test Structure
    2. Example Test from phosphor-logging
    3. Running phosphor-logging Tests
  9. Try It Yourself
    1. Standalone Examples (No OpenBMC Required)
    2. Real OpenBMC Test Suites
      1. phosphor-logging (Event Logging)
      2. dbus-sensors (Sensor Daemons)
      3. bmcweb (Redfish Server)
  10. Best Practices
    1. Test Organization
    2. Test Independence
    3. Mock Appropriately
    4. Coverage Goals
  11. Next Steps
  12. References

Overview

Unit testing is essential for OpenBMC development. All code submitted upstream must include unit tests. OpenBMC uses:

  • GTest - Google Test framework for C++ unit tests
  • GMock - Google Mock for creating mock objects
  • Meson - Build system with native test support

This guide covers writing tests, mocking D-Bus interfaces, and running tests in OpenBMC.

Why Unit Test?

Benefit Description
Upstream Requirement Code without tests is rejected
Catch Regressions Automated testing prevents bugs
Document Behavior Tests show expected usage
Enable Refactoring Safe code changes with test coverage

GTest/GMock Setup with Meson

Meson Configuration

Add test configuration to your service’s meson.build:

# Enable testing
gtest_dep = dependency('gtest', main: true, required: true)
gmock_dep = dependency('gmock', required: true)

# Define test executable
test_src = [
    'test/main_test.cpp',
    'test/service_test.cpp',
]

test_exe = executable(
    'test-myservice',
    test_src,
    dependencies: [
        gtest_dep,
        gmock_dep,
        sdbusplus_dep,
        phosphor_logging_dep,
    ],
    include_directories: include_directories('src'),
)

# Register test with Meson
test('myservice-tests', test_exe)

Project Structure

Organize test files alongside source:

my-service/
├── meson.build
├── src/
│   ├── main.cpp
│   └── service.cpp
└── test/
    ├── main_test.cpp      # GTest main entry
    ├── service_test.cpp   # Service unit tests
    └── mocks/
        └── mock_dbus.hpp  # D-Bus mocks

Test Main Entry Point

Create test/main_test.cpp:

#include <gtest/gtest.h>

int main(int argc, char** argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Writing GTest Tests

Basic Test Structure

#include <gtest/gtest.h>
#include "service.hpp"

// Simple test case
TEST(ServiceTest, InitializesCorrectly)
{
    Service svc;
    EXPECT_TRUE(svc.isInitialized());
}

// Test with expected value
TEST(ServiceTest, ReturnsDefaultValue)
{
    Service svc;
    EXPECT_EQ(svc.getValue(), 0);
}

Test Fixtures (TEST_F)

Use fixtures for common setup/teardown:

#include <gtest/gtest.h>
#include "sensor_manager.hpp"

class SensorManagerTest : public ::testing::Test
{
  protected:
    void SetUp() override
    {
        // Runs before each test
        manager = std::make_unique<SensorManager>();
    }

    void TearDown() override
    {
        // Runs after each test
        manager.reset();
    }

    std::unique_ptr<SensorManager> manager;
};

TEST_F(SensorManagerTest, AddsSensorSuccessfully)
{
    EXPECT_TRUE(manager->addSensor("CPU_Temp", 50.0));
    EXPECT_EQ(manager->getSensorCount(), 1);
}

TEST_F(SensorManagerTest, RejectsDuplicateSensor)
{
    manager->addSensor("CPU_Temp", 50.0);
    EXPECT_FALSE(manager->addSensor("CPU_Temp", 60.0));
}

Common Assertions

Assertion Description
EXPECT_TRUE(cond) Condition is true
EXPECT_FALSE(cond) Condition is false
EXPECT_EQ(a, b) Values are equal
EXPECT_NE(a, b) Values are not equal
EXPECT_LT(a, b) a < b
EXPECT_GT(a, b) a > b
EXPECT_THROW(expr, type) Expression throws exception
EXPECT_NO_THROW(expr) Expression doesn’t throw
ASSERT_* Same as EXPECT but stops test on failure

GMock Basics

Creating Mock Classes

#include <gmock/gmock.h>

// Interface to mock
class SensorInterface
{
  public:
    virtual ~SensorInterface() = default;
    virtual double getValue() = 0;
    virtual void setValue(double value) = 0;
    virtual bool isValid() const = 0;
};

// Mock implementation
class MockSensor : public SensorInterface
{
  public:
    MOCK_METHOD(double, getValue, (), (override));
    MOCK_METHOD(void, setValue, (double value), (override));
    MOCK_METHOD(bool, isValid, (), (const, override));
};

Using Mock Objects

#include <gmock/gmock.h>
#include <gtest/gtest.h>

using ::testing::Return;
using ::testing::_;

TEST(ControllerTest, ReadsFromSensor)
{
    MockSensor mockSensor;

    // Set up expectations
    EXPECT_CALL(mockSensor, getValue())
        .WillOnce(Return(45.5));

    Controller ctrl(&mockSensor);
    EXPECT_EQ(ctrl.readTemperature(), 45.5);
}

TEST(ControllerTest, HandlesSensorFailure)
{
    MockSensor mockSensor;

    EXPECT_CALL(mockSensor, isValid())
        .WillOnce(Return(false));

    Controller ctrl(&mockSensor);
    EXPECT_THROW(ctrl.readTemperature(), std::runtime_error);
}

Common GMock Matchers

Matcher Description
Return(value) Return specified value
_ Match any argument
Eq(value) Argument equals value
Gt(value) Argument greater than value
WillOnce(action) Perform action once
WillRepeatedly(action) Perform action every time
Times(n) Expect exactly n calls

D-Bus Mocking with sdbus++

Why Mock D-Bus?

Unit tests should be isolated. Mocking D-Bus:

  • Avoids requiring running D-Bus daemon
  • Controls responses for testing edge cases
  • Enables testing without real hardware

sdbus++ Mock Utilities

OpenBMC’s sdbus++ provides mock helpers. Include the mock headers:

#include <sdbusplus/test/sdbus_mock.hpp>

Example: Mocking D-Bus Method Calls

#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <sdbusplus/test/sdbus_mock.hpp>

using ::testing::_;
using ::testing::Return;

class DBusServiceTest : public ::testing::Test
{
  protected:
    sdbusplus::SdBusMock sdbusMock;

    void SetUp() override
    {
        // Set up mock bus
    }
};

TEST_F(DBusServiceTest, CallsRemoteMethod)
{
    // Expect D-Bus method call
    EXPECT_CALL(sdbusMock,
        sd_bus_call_method(_, _, _, _, _, _, _))
        .WillOnce(Return(0));

    auto bus = sdbusplus::get_mocked_new(&sdbusMock);
    MyService svc(bus);

    EXPECT_NO_THROW(svc.callRemoteMethod());
}

Mocking Property Gets/Sets

TEST_F(DBusServiceTest, GetsPropertyValue)
{
    // Mock property read
    EXPECT_CALL(sdbusMock,
        sd_bus_get_property(_, _, _, _, _, _, _))
        .WillOnce([](sd_bus*, const char*, const char*,
                     const char*, sd_bus_error*, sd_bus_message**,
                     const char*) {
            // Return mocked property value
            return 0;
        });

    auto bus = sdbusplus::get_mocked_new(&sdbusMock);
    MyService svc(bus);

    EXPECT_EQ(svc.getRemoteProperty(), expectedValue);
}

Verifying D-Bus Interactions

TEST_F(DBusServiceTest, EmitsSignalOnChange)
{
    EXPECT_CALL(sdbusMock,
        sd_bus_emit_signal(_, _, _, _, _))
        .Times(1);

    auto bus = sdbusplus::get_mocked_new(&sdbusMock);
    MyService svc(bus);

    svc.updateValue(newValue);  // Should emit signal
}

Running Tests

Via Meson (Local Build)

# Configure build with tests enabled
meson setup builddir -Dtests=enabled

# Build and run tests
meson test -C builddir

# Run with verbose output
meson test -C builddir -v

# Run specific test
meson test -C builddir myservice-tests

Via Bitbake

# Run tests for a specific recipe
bitbake phosphor-logging -c test

# Run tests and keep build directory
bitbake phosphor-logging -c test -f

# Check test results
cat tmp/work/*/phosphor-logging/*/temp/log.do_test

With SDK (Local Development)

# Source SDK environment
source /opt/openbmc-phosphor/*/environment-setup-*

# Build and test locally
meson setup builddir --cross-file=cross.txt
meson test -C builddir

Interpreting Test Output

[==========] Running 5 tests from 2 test suites.
[----------] 3 tests from ServiceTest
[ RUN      ] ServiceTest.InitializesCorrectly
[       OK ] ServiceTest.InitializesCorrectly (0 ms)
[ RUN      ] ServiceTest.ReturnsDefaultValue
[       OK ] ServiceTest.ReturnsDefaultValue (0 ms)
[ RUN      ] ServiceTest.HandlesError
[  FAILED  ] ServiceTest.HandlesError (1 ms)
[----------] 2 tests from ControllerTest
...
[==========] 5 tests from 2 test suites ran. (10 ms total)
[  PASSED  ] 4 tests.
[  FAILED  ] 1 test.

Test Coverage

Enabling Coverage in Meson

Add to meson.build:

if get_option('coverage')
    add_project_arguments('-fprofile-arcs', '-ftest-coverage',
                          language: 'cpp')
    add_project_link_arguments('-lgcov', language: 'cpp')
endif

Add to meson_options.txt:

option('coverage', type: 'boolean', value: false,
       description: 'Enable code coverage')

Generating Coverage Reports

# Build with coverage enabled
meson setup builddir -Dcoverage=true
meson compile -C builddir

# Run tests
meson test -C builddir

# Generate coverage report with lcov
lcov --capture --directory builddir \
     --output-file coverage.info

# Generate HTML report
genhtml coverage.info --output-directory coverage-report

# View report
xdg-open coverage-report/index.html

Coverage Report Interpretation

Metric Target Description
Line Coverage >80% Lines executed by tests
Function Coverage >90% Functions called by tests
Branch Coverage >70% Decision branches taken

Practical Example: phosphor-logging

The phosphor-logging repository demonstrates OpenBMC testing patterns.

Test Structure

phosphor-logging/
├── test/
│   ├── meson.build
│   ├── log_manager_test.cpp
│   ├── elog_test.cpp
│   └── remote_logging_test.cpp

Example Test from phosphor-logging

#include <gtest/gtest.h>
#include "log_manager.hpp"

class LogManagerTest : public ::testing::Test
{
  protected:
    void SetUp() override
    {
        // Create temporary log directory
        tempDir = std::filesystem::temp_directory_path() / "test-logs";
        std::filesystem::create_directories(tempDir);
    }

    void TearDown() override
    {
        std::filesystem::remove_all(tempDir);
    }

    std::filesystem::path tempDir;
};

TEST_F(LogManagerTest, CreatesLogEntry)
{
    LogManager manager(tempDir);

    auto id = manager.createEntry(
        "Test message",
        Entry::Level::Informational);

    EXPECT_GT(id, 0);
    EXPECT_TRUE(manager.hasEntry(id));
}

TEST_F(LogManagerTest, DeletesLogEntry)
{
    LogManager manager(tempDir);
    auto id = manager.createEntry("Test", Entry::Level::Error);

    EXPECT_TRUE(manager.deleteEntry(id));
    EXPECT_FALSE(manager.hasEntry(id));
}

Running phosphor-logging Tests

# Clone and build
git clone https://github.com/openbmc/phosphor-logging
cd phosphor-logging

# Build with tests
meson setup builddir -Dtests=enabled
meson compile -C builddir

# Run tests
meson test -C builddir -v

Try It Yourself

Standalone Examples (No OpenBMC Required)

Build and run the example tests from this tutorial:

# Clone the tutorial repository
git clone https://github.com/MichaelTien8901/openbmc-guide-tutorial.git
cd openbmc-guide-tutorial/docs/examples/testing

# Option 1: CMake (auto-downloads GTest)
mkdir build && cd build
cmake ..
make
ctest --output-on-failure

# Option 2: Make (requires GTest installed)
# sudo apt install libgtest-dev libgmock-dev
make
make test

Expected output:

[==========] Running 7 tests from 2 test suites.
[----------] 2 tests from SensorManagerBasicTest
[ RUN      ] SensorManagerBasicTest.InitiallyEmpty
[       OK ] SensorManagerBasicTest.InitiallyEmpty (0 ms)
...
[  PASSED  ] 7 tests.

Real OpenBMC Test Suites

After learning the basics, try running tests from actual OpenBMC repositories:

phosphor-logging (Event Logging)

git clone https://github.com/openbmc/phosphor-logging
cd phosphor-logging

# Install dependencies (on Ubuntu)
sudo apt install meson ninja-build pkg-config \
    libsdbusplus-dev libphosphor-dbus-interfaces-dev \
    libcereal-dev nlohmann-json3-dev

# Build with tests
meson setup builddir -Dtests=enabled
meson compile -C builddir

# Run tests
meson test -C builddir -v

dbus-sensors (Sensor Daemons)

git clone https://github.com/openbmc/dbus-sensors
cd dbus-sensors
meson setup builddir -Dtests=enabled
meson compile -C builddir
meson test -C builddir -v

bmcweb (Redfish Server)

git clone https://github.com/openbmc/bmcweb
cd bmcweb
meson setup builddir -Dtests=enabled
meson compile -C builddir
meson test -C builddir -v

If dependency installation is complex, use the OpenBMC SDK which includes all required libraries pre-installed.


Best Practices

Test Organization

  • One test file per source file (service.cppservice_test.cpp)
  • Group related tests in test fixtures
  • Use descriptive test names: TEST(Component, WhatItDoes)

Test Independence

  • Each test should be independent
  • Don’t rely on test execution order
  • Clean up resources in TearDown

Mock Appropriately

  • Mock external dependencies (D-Bus, filesystem, network)
  • Don’t mock the code under test
  • Verify mock expectations

Coverage Goals

  • Aim for >80% line coverage
  • Test error paths, not just happy paths
  • Cover boundary conditions

Next Steps


References


Tested on: OpenBMC master branch with Meson build system


Back to top

OpenBMC Guide Tutorial is not affiliated with the OpenBMC project. Content is provided for educational purposes.

This site uses Just the Docs, a documentation theme for Jekyll.