Chapter 12: Unit testing with score::mw::com mocks#
In this chapter we show how to unit test score::mw::com based code in classic gtest/gmock style. The chapter is based on the HelloWorld provider/consumer applications from chapter 2, but both applications were adapted to improve unit-testability.
Why we introduced ProxyComponent / SkeletonComponent#
The important idea is to separate:
the runtime/bootstrap part (service discovery, static Create(…), wiring), and
the business interaction part (subscribe/get samples or allocate/send samples).
For the consumer, this split is done via ProxyComponent, which takes an already created HelloWorldProxy:
class ProxyComponent
{
public:
ProxyComponent(HelloWorldProxy&& hello_world_proxy)
: hello_world_proxy_(std::move(hello_world_proxy)), receive_counter_(0)
{
}
bool Subscribe();
void GetNewSamples();
size_t GetReceiveCounter() const;
private:
std::optional<HelloWorldProxy> hello_world_proxy_;
size_t receive_counter_;
};
For the provider, this split is done via SkeletonComponent, which takes an already created HelloWorldSkeleton:
class SkeletonComponent
{
public:
enum class SendSampleResult
{
kHandled,
kSendFailed,
kNoSampleAllocated,
kFatalError,
};
explicit SkeletonComponent(HelloWorldSkeleton&& hello_world_skeleton)
: hello_world_skeleton_(std::move(hello_world_skeleton))
{
}
bool OfferService();
SendSampleResult SendSample(std::size_t send_counter);
private:
std::optional<HelloWorldSkeleton> hello_world_skeleton_;
This enables focused unit tests with injected mocks for the proxy/skeleton objects.
Current mocking boundaries (important)#
At the time of writing, mocking support starts after proxy/skeleton creation:
No service-discovery mocking support for Proxy::FindService() / Proxy::StartFindService() in this tutorial context.
No mocking support for static `Create(…)` APIs on proxy and skeleton in this chapter’s application flow.
Therefore, both applications are structured so that testable logic begins at:
consumer: after HelloWorldProxy::Create(…), where ProxyComponent is constructed.
provider: after HelloWorldSkeleton::Create(…), where SkeletonComponent is constructed.
You can see that cut in the consumer application:
auto service_handle_container_result =
score::mw::com::tutorial::HelloWorldProxy::FindService(instance_specifier.value());
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(service_handle_container_result.has_value(),
"HelloWorldProxy::FindService failed!");
if (service_handle_container_result.value().empty())
{
std::cout << "No HelloWorld service instance found yet. Retrying..." << std::endl;
}
else
{
std::cout << "HelloWorld service instance found." << std::endl;
service_handle = service_handle_container_result.value()[0];
break;
}
}
auto proxy_result = score::mw::com::tutorial::HelloWorldProxy::Create(service_handle.value());
if (!proxy_result)
{
std::cerr << "Failed to create HelloWorldProxy: " << proxy_result.error() << std::endl;
exit(1);
}
score::mw::com::tutorial::ProxyComponent proxy_component(std::move(proxy_result).value());
… and on provider side:
auto hello_world_service_instance_result =
score::mw::com::tutorial::HelloWorldSkeleton::Create(instance_specifier.value());
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(hello_world_service_instance_result.has_value(),
"Failed to create HelloWorldSkeleton instance!");
auto hello_world_service_instance = std::move(hello_world_service_instance_result).value();
score::mw::com::tutorial::SkeletonComponent skeleton_component{std::move(hello_world_service_instance)};
if (!skeleton_component.OfferService())
Including the public test types (AoU-compliant)#
The unit tests only include public score::mw::com headers. In particular, they must not include headers from
score/mw/com/impl/* directly and must not use the score::mw::com::impl namespace, because these are
implementation details and are not part of the public API (relying on them would violate the Assumptions of Use / AoU
of score::mw::com).
All mocking types the tests need are therefore pulled in via the single public header
score/mw/com/test_types.h.
It exposes public aliases in the score::mw::com namespace - e.g. ProxyEventMock, NamedProxyEventMock,
ProxyWrapperClassTestView, SkeletonEventMock, NamedSkeletonEventMock, SkeletonWrapperClassTestView and
SkeletonBaseMock - as well as helpers such as MakeFakeSamplePtr and MakeFakeSampleAllocateePtr. These
aliases resolve to the corresponding score::mw::com::impl types, so the tests can use them through the public
score::mw::com namespace without ever depending on impl headers or the impl namespace.
#include "score/mw/com/doc/tutorial/chapter_12/consumer/proxy_component.h"
#include "score/mw/com/com_error_domain.h"
#include "score/mw/com/test_types.h"
#include <gtest/gtest.h>
using testing::_;
using testing::Invoke;
using testing::Return;
The provider-side test uses the same approach:
#include "score/mw/com/doc/tutorial/chapter_12/provider/skeleton_component.h"
#include "score/mw/com/com_error_domain.h"
#include "score/mw/com/test_types.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <tuple>
Consumer side unit test approach (ProxyComponent)#
The ProxyComponent implementation encapsulates subscribe/get-samples behavior:
bool ProxyComponent::Subscribe()
{
auto result = hello_world_proxy_->message.Subscribe(1);
if (!result)
{
std::cerr << "Failed to subscribe to HelloWorldService instance 'message' event: " << result.error()
<< std::endl;
return false;
}
return true;
}
void ProxyComponent::GetNewSamples()
{
auto get_new_samples_result = hello_world_proxy_->message.GetNewSamples(
[this](auto&& sample) {
const auto* buf = sample.Get()->data();
std::cout << "Sample received. Event \"message\" update received: " << buf << std::endl;
++receive_counter_;
},
1);
if (!get_new_samples_result)
{
std::cerr << "Failed to get new samples: " << get_new_samples_result.error() << std::endl;
}
}
size_t ProxyComponent::GetReceiveCounter() const
{
return receive_counter_;
}
Its unit tests use ProxyWrapperClassTestView and injected event mocks:
class ProxyComponentTestFixture : public ::testing::Test
{
protected:
using ProxyComponent = score::mw::com::tutorial::ProxyComponent;
using MessageEventMock = score::mw::com::ProxyEventMock<score::mw::com::tutorial::HelloWorldProxy::FixedSizeString>;
void SetUp() override
{
auto proxy =
score::mw::com::ProxyWrapperClassTestView<score::mw::com::tutorial::HelloWorldProxy>::Create(events_tuple_);
// Note: This additional (redundant as we handed it over in the Create() already) "inject" of the event mock
// into the proxy mock is currently needed, but will be removed in the future.
proxy.message.InjectMock(message_event_mock());
proxy_component_.emplace(std::move(proxy));
}
MessageEventMock& message_event_mock()
{
return std::get<0>(events_tuple_).mock;
}
std::tuple<score::mw::com::NamedProxyEventMock<score::mw::com::tutorial::HelloWorldProxy::FixedSizeString>>
events_tuple_{"message"};
std::optional<ProxyComponent> proxy_component_{};
};
Test cases then follow the given/expect/when/then style:
TEST_F(ProxyComponentTestFixture, SubscribeReturnsTrueWhenEventSubscribeSucceeds)
{
// Given a mock for our message event
auto& event_mock = message_event_mock();
// Expect, that Subscribe with max_samples = 1 is called on the message ProxyEvent and it returns success
EXPECT_CALL(event_mock, Subscribe(1)).WillOnce(Return(score::Result<void>{}));
// when we call Subscribe on the unit under test
auto subscribe_result = proxy_component_->Subscribe();
// and that the result of the Subscribe is true.
EXPECT_TRUE(subscribe_result);
}
TEST_F(ProxyComponentTestFixture, SubscribeReturnsFalseWhenEventSubscribeFails)
{
// Given a mock for our message event
auto& event_mock = message_event_mock();
// Expect, that Subscribe with max_samples = 1 is called on the message ProxyEvent, and it returns a binding failure
EXPECT_CALL(event_mock, Subscribe(1))
.WillOnce(Return(score::MakeUnexpected(score::mw::com::ComErrc::kBindingFailure)));
// when we call Subscribe on the unit under test
auto subscribe_result = proxy_component_->Subscribe();
// and that the result of the Subscribe is false.
EXPECT_FALSE(subscribe_result);
}
TEST_F(ProxyComponentTestFixture, ReceiveCounterIncrementedWhenGetNewSamplesSucceeds)
{
// Given a mock for our message event
auto& event_mock = message_event_mock();
// expect, that a call to ProxyEvent::GetNewSamples takes place with a max_samples_count being 1
EXPECT_CALL(event_mock, GetNewSamples(_, 1))
.WillOnce(Invoke([](auto&& receiver, std::size_t) -> score::Result<std::size_t> {
// Simulate receiving a sample by invoking the receiver callback with a dummy sample containing "Hello
// World". This isn't checked by our UuT - but we simply show the complete steps, how to create a SamplePtr
// in tests and fill it with data.
// Creating a unique_ptr pointing at the sample-data
constexpr std::string_view kHelloWorld{"Hello World"};
auto sample_unique_ptr = std::make_unique<score::mw::com::tutorial::HelloWorldProxy::FixedSizeString>();
std::memcpy(sample_unique_ptr->data(), kHelloWorld.data(), kHelloWorld.size());
// Creating a fake SamplePtr from the unique_ptr
auto sample_ptr = score::mw::com::MakeFakeSamplePtr(std::move(sample_unique_ptr));
// Calling the user provided (proxy component provided) receiver.
receiver(std::move(sample_ptr));
// this is the return of the GetNewSamples call - indicating how many samples have been received (how many
// times the receiver callback has been called)
return 1;
}));
// when we call GetNewSamples on the unit under test
proxy_component_->GetNewSamples();
// and the receive counter is incremented to 1
EXPECT_EQ(proxy_component_->GetReceiveCounter(), 1U);
}
Provider side unit test approach (SkeletonComponent)#
The SkeletonComponent implementation encapsulates offer/allocate/send behavior:
bool SkeletonComponent::OfferService()
{
auto offer_result = hello_world_skeleton_->OfferService();
if (!offer_result)
{
std::cerr << "Failed to offer HelloWorldSkeleton instance: " << offer_result.error() << std::endl;
return false;
}
return true;
}
SkeletonComponent::SendSampleResult SkeletonComponent::SendSample(std::size_t send_counter)
{
auto sample_allocatee_ptr = hello_world_skeleton_->message.Allocate();
if (!sample_allocatee_ptr)
{
std::cerr << "Failed to allocate sample_allocatee_ptr: " << sample_allocatee_ptr.error() << std::endl;
return SendSampleResult::kNoSampleAllocated;
}
std::memcpy(sample_allocatee_ptr.value().Get()->data(), kHelloWorld.data(), kHelloWorld.size());
auto* buf = sample_allocatee_ptr.value().Get()->data();
const auto remaining = sample_allocatee_ptr.value().Get()->size() - kHelloWorld.size();
const auto chars_written = std::snprintf(buf + kHelloWorld.size(), remaining, "%zu", send_counter);
if (chars_written < 0)
{
std::cerr << "Failed to write 'send_counter' to sample_allocatee_ptr!" << std::endl;
return SendSampleResult::kFatalError;
}
const std::string payload_for_log{buf};
auto send_result = hello_world_skeleton_->message.Send(std::move(sample_allocatee_ptr.value()));
if (!send_result)
{
std::cerr << "Failed to send sample_allocatee_ptr: " << send_result.error() << std::endl;
return SendSampleResult::kSendFailed;
}
std::cout << "Sample send completed. Event \"message\" update sent: " << payload_for_log << std::endl;
return SendSampleResult::kHandled;
}
Its unit tests use SkeletonWrapperClassTestView, NamedSkeletonEventMock, SkeletonEventMock, and SkeletonBaseMock:
class SkeletonComponentTestFixture : public ::testing::Test
{
protected:
using SkeletonComponent = score::mw::com::tutorial::SkeletonComponent;
using FixedSizeString = score::mw::com::tutorial::HelloWorldProxy::FixedSizeString;
using MessageEventMock = score::mw::com::SkeletonEventMock<FixedSizeString>;
void SetUp() override
{
auto skeleton =
score::mw::com::SkeletonWrapperClassTestView<score::mw::com::tutorial::HelloWorldSkeleton>::Create(
skeleton_mock_, events_tuple_);
skeleton_component_.emplace(std::move(skeleton));
}
MessageEventMock& message_event_mock()
{
return std::get<0>(events_tuple_).mock;
}
score::mw::com::SkeletonBaseMock skeleton_mock_{};
std::tuple<score::mw::com::NamedSkeletonEventMock<FixedSizeString>> events_tuple_{"message"};
std::optional<SkeletonComponent> skeleton_component_{};
};
… with given/expect/when/then style test cases:
TEST_F(SkeletonComponentTestFixture, OfferServiceReturnsTrueWhenOfferServiceSucceeds)
{
// Expect, that OfferService is called on the mocked SkeletonBase and returns success
EXPECT_CALL(skeleton_mock_, OfferService()).WillOnce(Return(score::Result<void>{}));
// when we call OfferService on the unit under test
EXPECT_TRUE(skeleton_component_->OfferService());
// and that the result is true.
}
TEST_F(SkeletonComponentTestFixture, OfferServiceReturnsFalseWhenOfferServiceFails)
{
// Expect, that OfferService is called on the mocked SkeletonBase and returns a binding failure
EXPECT_CALL(skeleton_mock_, OfferService())
.WillOnce(Return(score::MakeUnexpected(score::mw::com::ComErrc::kBindingFailure)));
// when we call OfferService on the unit under test
EXPECT_FALSE(skeleton_component_->OfferService());
// and that the result is false.
}
TEST_F(SkeletonComponentTestFixture, SendSampleReturnsNoSampleAllocatedWhenAllocateFails)
{
// Expect, that Allocate is called on the mocked SkeletonEvent and returns a binding failure
EXPECT_CALL(message_event_mock(), Allocate())
.WillOnce(Return(score::MakeUnexpected(score::mw::com::ComErrc::kBindingFailure)));
// when we call SendSample on the unit under test
EXPECT_EQ(skeleton_component_->SendSample(5U), SkeletonComponent::SendSampleResult::kNoSampleAllocated);
// and that the result indicates that no sample slot was allocated.
}
TEST_F(SkeletonComponentTestFixture, SendSampleReturnsSendFailedWhenSendFails)
{
// Expect, that Allocate returns a sample slot
EXPECT_CALL(message_event_mock(), Allocate())
.WillOnce(Invoke([]() -> score::Result<score::mw::com::SampleAllocateePtr<FixedSizeString>> {
auto sample_unique_ptr = std::make_unique<FixedSizeString>();
return score::mw::com::MakeFakeSampleAllocateePtr(std::move(sample_unique_ptr));
}));
// and expect, that Send is called and returns a binding failure
EXPECT_CALL(message_event_mock(), Send(testing::A<score::mw::com::SampleAllocateePtr<FixedSizeString>>()))
.WillOnce(Return(score::MakeUnexpected(score::mw::com::ComErrc::kBindingFailure)));
// when we call SendSample on the unit under test
EXPECT_EQ(skeleton_component_->SendSample(42U), SkeletonComponent::SendSampleResult::kSendFailed);
// and that the result indicates a send failure.
}
TEST_F(SkeletonComponentTestFixture, SendSampleWritesPayloadAndReturnsHandledWhenSendSucceeds)
{
// Expect, that Allocate returns a sample slot
EXPECT_CALL(message_event_mock(), Allocate())
.WillOnce(Invoke([]() -> score::Result<score::mw::com::SampleAllocateePtr<FixedSizeString>> {
auto sample_unique_ptr = std::make_unique<FixedSizeString>();
return score::mw::com::MakeFakeSampleAllocateePtr(std::move(sample_unique_ptr));
}));
// and expect, that Send is called with a payload containing "Hello World42"
EXPECT_CALL(message_event_mock(), Send(testing::A<score::mw::com::SampleAllocateePtr<FixedSizeString>>()))
.WillOnce(
Invoke([](score::mw::com::SampleAllocateePtr<FixedSizeString> sample_allocatee_ptr) -> score::Result<void> {
EXPECT_STREQ(sample_allocatee_ptr.Get()->data(), "Hello World42");
return {};
}));
// when we call SendSample on the unit under test
EXPECT_EQ(skeleton_component_->SendSample(42U), SkeletonComponent::SendSampleResult::kHandled);
// and that the result indicates successful handling.
}
Bazel test targets#
Both unit tests are integrated as dedicated cc_unit_test targets:
You can run them via:
bazel test //score/mw/com/doc/tutorial/chapter_12:proxy_component_test
bazel test //score/mw/com/doc/tutorial/chapter_12:skeleton_component_test
Where to look for broader mocking capabilities#
For an overview of currently supported mocking features (beyond this chapter’s specific example), inspect the mocking tests under:
score/mw/com/impl/mocking/*_test.cpp
Good entry points are, for example:
Files/artifacts used#
The bazel project for this chapter is located in score/mw/com/doc/tutorial/chapter_12.
File Name |
Description |
|---|---|
Bazel targets for provider/consumer apps and unit tests. |
|
Service interface and proxy/skeleton aliases. |
|
Translation unit for the service interface. |
|
Consumer app wiring (discovery/create + ProxyComponent usage). |
|
Consumer-side wrapper around the proxy. |
|
ProxyComponent implementation. |
|
Unit tests using proxy-side mocking infrastructure. |
|
Provider app wiring (create + SkeletonComponent usage). |
|
Provider-side wrapper around the skeleton. |
|
SkeletonComponent implementation. |
|
Unit tests using skeleton-side mocking infrastructure. |
|
Consumer runtime configuration. |
|
Provider runtime configuration. |
|
Consumer logging configuration. |
|
Provider logging configuration. |
Summary#
A short summary of what this chapter showed:
Chapter 12 is based on chapter 2 HelloWorld apps, but refactored for unit-testability.
ProxyComponent and SkeletonComponent isolate proxy/skeleton interaction logic into small testable units.
Tests only depend on the public score::mw::com API: mocking types are included via score/mw/com/test_types.h, so no score/mw/com/impl/* headers and no score::mw::com::impl namespace are used (AoU-compliant).
Unit tests use ProxyWrapperClassTestView (consumer side) and SkeletonWrapperClassTestView (provider side), with injected event/skeleton mocks in gtest/gmock style.
Current limitation: service discovery and static Create(…) calls are not mocked in this chapter flow, so tests start after proxy/skeleton instances already exist.
For deeper coverage of supported mocking features, inspect score/mw/com/impl/mocking/*_test.cpp.