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:

  1. the runtime/bootstrap part (service discovery, static Create(…), wiring), and

  2. 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:

Listing 86 consumer/proxy_component.h#
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:

Listing 87 provider/skeleton_component.h#
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:

Listing 88 consumer/consumer.cpp#
        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:

Listing 89 provider/provider.cpp#
    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.

Listing 90 consumer/proxy_component_test.cpp (public includes)#
#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:

Listing 91 provider/skeleton_component_test.cpp (public includes)#
#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:

Listing 92 consumer/proxy_component.cpp#
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:

Listing 93 consumer/proxy_component_test.cpp#
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:

Listing 94 consumer/proxy_component_test.cpp#
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:

Listing 95 provider/skeleton_component.cpp#
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:

Listing 96 provider/skeleton_component_test.cpp#
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:

Listing 97 provider/skeleton_component_test.cpp#
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

BUILD

Bazel targets for provider/consumer apps and unit tests.

hello_world_service.h

Service interface and proxy/skeleton aliases.

hello_world_service.cpp

Translation unit for the service interface.

consumer/consumer.cpp

Consumer app wiring (discovery/create + ProxyComponent usage).

consumer/proxy_component.h

Consumer-side wrapper around the proxy.

consumer/proxy_component.cpp

ProxyComponent implementation.

consumer/proxy_component_test.cpp

Unit tests using proxy-side mocking infrastructure.

provider/provider.cpp

Provider app wiring (create + SkeletonComponent usage).

provider/skeleton_component.h

Provider-side wrapper around the skeleton.

provider/skeleton_component.cpp

SkeletonComponent implementation.

provider/skeleton_component_test.cpp

Unit tests using skeleton-side mocking infrastructure.

consumer/mw_com_config.json

Consumer runtime configuration.

provider/mw_com_config.json

Provider runtime configuration.

consumer/logging.json

Consumer logging configuration.

provider/logging.json

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.