-
Notifications
You must be signed in to change notification settings - Fork 0
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.
The test engine is included in the serp-dev package and is enabled by building with:
SERP_BUILD_TEST_ENGINE=ONCMake 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.
The central fixture helper. It:
- Creates and owns an
EventLoopfor 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.
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().
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 |
#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()callsonInitialize()on all created services and blocks until they complete. -
runner.deinitialize()callsonDeinitialize()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.
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);
}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.
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>).
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.
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.");
}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)See serp-demo/component_test/ in the serp-demo repository for a complete working example including service setup, mock injection, and reply capture.
-
Services and Lifecycle — lifecycle callbacks that
TestRunnerdrives - Methods — method signatures your tests call
- Runtime and Debug Tools — interactive debugging complement to automated tests
Getting Started
The Development Model
Architecture Language
Code Generator
- Generator Overview
- Generated Code Layout
- Deployment Configurations
- Lifecycle Backends
- CMake Integration
Framework Internals
- Core Concepts
- Services & Lifecycle
- Methods
- Properties
- Notifications
- Timers & Watchdog
- Promises & Async
- Streams
- Commands
- Logging
- Test Engine
- Transports
- Runtime & Debug Tools
VS Code Plugin
Examples