Skip to content

Test Engine

Oleksandr Geronime edited this page Jun 27, 2026 · 2 revisions

Test Engine

serp::test_engine is a component testing framework built on top of Google Test. It gives service-level tests a managed event loop, controlled service lifecycle, async reply capture, and structured assertion helpers — without requiring a live transport or a full process deployment.


Availability

The test engine is included in the serp-dev package and is enabled by building with:

SERP_BUILD_TEST_ENGINE=ON

CMake target: Serp::test_engine

target_link_libraries(my_tests PRIVATE Serp::test_engine GTest::gtest_main)

The test engine is a development dependency. Do not link it into production binaries.


Key Classes

serp::TestRunner

The central fixture helper. It:

  • Creates and owns an EventLoop for the test.
  • Manages the full service lifecycle (initialize → test → deinitialize).
  • Enforces per-test timeouts so a stalled async call fails the test rather than hanging the suite.
  • Provides waitFor() to block the test thread until an async reply arrives.

serp::ReplyHolder<T>

A typed capture object for async replies. Pass reply.callback() wherever a service method expects its reply callback. Call runner.waitFor(reply, timeout) to block until the reply arrives, then assert on reply.value().

serp::PromiseTest

Validation helpers for asserting on string-typed or structured replies using check types:

Check type Meaning
EQUAL Exact string match
START Reply starts with the given prefix
END Reply ends with the given suffix
CONTAIN Reply contains the given substring
REGEX Reply matches a regular expression
JSON Reply parses as JSON and matches the expected structure

Basic Test Pattern

#include <serp/test_engine/TestRunner.hpp>
#include <gtest/gtest.h>

class MyServiceTest : public testing::Test {
protected:
    serp::TestRunner runner;
    std::shared_ptr<MyService> service;

    void SetUp() override {
        service = runner.create<MyService>();
        runner.initialize();
    }

    void TearDown() override {
        runner.deinitialize();
    }
};

TEST_F(MyServiceTest, BasicMethod) {
    auto reply = serp::ReplyHolder<std::string>();

    service->greet("world", reply.callback());

    runner.waitFor(reply, std::chrono::seconds(2));

    EXPECT_EQ(reply.value(), "Hello, world!");
}

Key points:

  • runner.create<MyService>() constructs the service on the test event loop, not on a real service thread. The service behaves exactly as it would in production but is driven by the test runner.
  • runner.initialize() calls onInitialize() on all created services and blocks until they complete.
  • runner.deinitialize() calls onDeinitialize() on all services and blocks until they complete.
  • runner.waitFor(reply, timeout) pumps the event loop until the reply arrives or the timeout expires. If the timeout expires, the test fails with a descriptive message.

Testing Multiple Reply Values

ReplyHolder captures the most recent value. For testing a sequence of replies, use multiple holders or assert inside the callback:

TEST_F(MyServiceTest, SequentialCalls) {
    auto r1 = serp::ReplyHolder<int>();
    auto r2 = serp::ReplyHolder<int>();

    service->add(2, 3, r1.callback());
    runner.waitFor(r1, std::chrono::seconds(1));
    EXPECT_EQ(r1.value(), 5);

    service->multiply(4, 6, r2.callback());
    runner.waitFor(r2, std::chrono::seconds(1));
    EXPECT_EQ(r2.value(), 24);
}

Idle Checking

Verify that the event loop is not blocked or stalled after an operation:

TEST_F(MyServiceTest, EventLoopIdle) {
    service->triggerBackgroundWork();

    runner.waitIdle(std::chrono::milliseconds(500));
    // Event loop processed all queued callbacks within 500ms
}

waitIdle() returns once no more callbacks are pending on the test event loop. Use it to ensure that a triggered operation has fully completed before asserting on side effects.


Mocking Dependencies

Services that depend on other services can be tested with generated mock objects:

class MyServiceTest : public testing::Test {
protected:
    serp::TestRunner runner;
    std::shared_ptr<MockDependencyService> mockDep;
    std::shared_ptr<MyService> service;

    void SetUp() override {
        // Create mock before the service that depends on it
        mockDep = runner.create<MockDependencyService>();
        service = runner.create<MyService>(mockDep);

        // Set expectations before initialize()
        EXPECT_CALL(*mockDep, fetchData(testing::_, testing::_))
            .WillOnce(testing::InvokeArgument<1>(42));

        runner.initialize();
    }

    void TearDown() override {
        runner.deinitialize();
    }
};

Mocks are generated from SerpIDL interfaces alongside the real service stubs. The generated mock class follows the Google Mock naming convention (Mock<InterfaceName>).


Method Receive / Invoke Simulation

TestRunner can simulate inbound calls to a service without a real transport client:

TEST_F(MyServiceTest, SimulatedInboundCall) {
    auto reply = serp::ReplyHolder<std::string>();

    // Drive the service as if a remote caller invoked the method
    runner.invoke(*service, &MyService::processRequest, "input_data", reply.callback());

    runner.waitFor(reply, std::chrono::seconds(2));
    EXPECT_EQ(reply.value(), "processed: input_data");
}

This dispatches the call on the test event loop, exactly as the transport layer would in production.


Structured Assertions with PromiseTest

TEST_F(MyServiceTest, JsonReply) {
    auto reply = serp::ReplyHolder<std::string>();

    service->getStatus(reply.callback());
    runner.waitFor(reply, std::chrono::seconds(1));

    serp::PromiseTest::check(reply.value(),
        serp::CheckType::JSON,
        R"({"status":"ok"})");
}

TEST_F(MyServiceTest, PrefixCheck) {
    auto reply = serp::ReplyHolder<std::string>();

    service->getVersion(reply.callback());
    runner.waitFor(reply, std::chrono::seconds(1));

    serp::PromiseTest::check(reply.value(),
        serp::CheckType::START,
        "v2.");
}

CMake Integration

find_package(Serp REQUIRED COMPONENTS test_engine)
find_package(GTest REQUIRED)

add_executable(my_service_tests
    component_test/MyServiceTest.cpp
)

target_link_libraries(my_service_tests PRIVATE
    my_service_lib
    Serp::test_engine
    GTest::gtest_main
)

include(GoogleTest)
gtest_discover_tests(my_service_tests)

Example Project

See serp-demo/component_test/ in the serp-demo repository for a complete working example including service setup, mock injection, and reply capture.


See Also

Clone this wiki locally