Unit testing#
pw_rpc2: Next gen, zero-copy async RPC with end-to-end backpressure
pw::rpc2::test::TestHarness is a lightweight dependency-injection fixture
for unit testing pw_rpc2 services and clients with minimal boilerplate.
Constructing TestHarness<MyService> instantiates MyService (forwarding
any constructor arguments) and connects a generated service client to it. You
can use the harness from either side:
Testing a service: Call RPCs through
tester.service_client()(orpwpb_client()/raw_client()) and assert on the responses or ontester.service().TestHarness<MyService> tester(arg1, arg2); auto resp = tester.RunToCompletion(tester.service_client().DoSomething(req)); PW_TEST_ASSERT_OK(resp); EXPECT_EQ(tester.service().call_count(), 1u);
Testing client code: Pass
tester.service_client()ortester.client()to the client code under test and use a testServiceImplto verify how the client interacts with the service.TestHarness<FakeMyService> tester; MyClientComponent component(tester.service_client()); PW_TEST_ASSERT_OK(tester.RunToCompletion(component.FetchStatus())); EXPECT_TRUE(tester.service().was_queried());
Setup#
Add the test harness and generated RPC library to your unit test target:
load("//pw_unit_test:pw_cc_test.bzl", "pw_cc_test")
pw_cc_test(
name = "my_service_test",
srcs = ["my_service_test.cc"],
deps = [
":my_service",
":my_service_pwpb_rpc2",
"@pigweed//pw_rpc2/cpp:test_harness",
],
)
import("$dir_pw_unit_test/test.gni")
pw_test("my_service_test") {
sources = [ "my_service_test.cc" ]
deps = [
":my_service",
":my_service_proto.pwpb_rpc2",
"$dir_pw_rpc2/cpp:test_harness",
]
}
pw_add_test(my_service_test
SOURCES
my_service_test.cc
PRIVATE_DEPS
my_service
my_service_proto.pwpb_rpc2
pw_rpc2.cpp.test_harness
)
Include pw_rpc2/test_harness.h alongside your service implementation and
generated RPC header:
#include "my_service.h"
#include "my_service.pwpb.rpc2.h"
#include "pw_rpc2/test_harness.h"
#include "pw_unit_test/framework.h"
Testing with coroutines#
Passing a coroutine lambda to RunToCompletion() lets a test co_await
unary and streaming RPCs sequentially:
TEST(TestHarnessTest, UnaryCoroClient) {
TestHarness<EchoFutureService> tester;
tester.RunToCompletion([&](CoroContext) -> Coro<void> {
RpcResult<EchoResponse> resp =
co_await tester.service_client().EchoUnary(EchoRequest{.val = 21});
PW_TEST_EXPECT_OK(resp);
if (resp.ok()) {
EXPECT_EQ(resp->val, 42u);
}
});
}
TEST(TestHarnessTest, ServerStreamingCoroClient) {
TestHarness<EchoFutureService> tester;
tester.RunToCompletion([&](CoroContext) -> Coro<void> {
RpcResult<Reader<EchoResponse>> reader =
co_await tester.service_client().CountUpServerStream(
EchoRequest{.val = 100});
PW_TEST_EXPECT_OK(reader);
if (!reader.ok()) {
co_return;
}
for (uint32_t expected = 101; expected <= 103; ++expected) {
RpcResult<EchoResponse> msg = co_await reader->Read();
PW_TEST_EXPECT_OK(msg);
if (msg.ok()) {
EXPECT_EQ(msg->val, expected);
}
}
EXPECT_EQ((co_await reader->Read()).status(), RpcStatus::EndOfStream());
});
}
TEST(TestHarnessTest, ClientStreamingCoroClient) {
TestHarness<EchoFutureService> tester;
tester.RunToCompletion([&](CoroContext) -> Coro<void> {
auto call = co_await tester.service_client().AccumulateClientStream();
PW_TEST_EXPECT_OK(call);
if (!call.ok()) {
co_return;
}
PW_TEST_EXPECT_OK(co_await call->writer().Write(EchoRequest{.val = 10}));
PW_TEST_EXPECT_OK(co_await call->writer().Write(EchoRequest{.val = 20}));
PW_TEST_EXPECT_OK(co_await call->writer().Write(EchoRequest{.val = 30}));
PW_TEST_EXPECT_OK(co_await call->writer().Finish());
RpcResult<EchoResponse> resp = co_await call->response();
PW_TEST_EXPECT_OK(resp);
if (resp.ok()) {
EXPECT_EQ(resp->val, 60u);
}
});
}
TEST(TestHarnessTest, BidiStreamingCoroClient) {
TestHarness<EchoFutureService> tester;
tester.RunToCompletion([&](CoroContext) -> Coro<void> {
auto call = co_await tester.service_client().EchoBidiStream();
PW_TEST_EXPECT_OK(call);
if (!call.ok()) {
co_return;
}
PW_TEST_EXPECT_OK(co_await call->writer().Write(EchoRequest{.val = 5}));
RpcResult<EchoResponse> first = co_await call->reader().Read();
PW_TEST_EXPECT_OK(first);
if (first.ok()) {
EXPECT_EQ(first->val, 10u);
}
PW_TEST_EXPECT_OK(co_await call->writer().Write(EchoRequest{.val = 15}));
RpcResult<EchoResponse> second = co_await call->reader().Read();
PW_TEST_EXPECT_OK(second);
if (second.ok()) {
EXPECT_EQ(second->val, 30u);
}
PW_TEST_EXPECT_OK(co_await call->writer().Finish());
EXPECT_EQ((co_await call->reader().Read()).status(),
RpcStatus::EndOfStream());
});
}
Testing with futures and tasks#
Pass an RPC future directly to RunToCompletion():
TEST(TestHarnessTest, UnaryDirectFuture) {
TestHarness<EchoFutureService> tester;
RpcResult<EchoResponse> resp = tester.RunToCompletion(
tester.service_client().EchoUnary(EchoRequest{.val = 21}));
PW_TEST_ASSERT_OK(resp);
EXPECT_EQ(resp->val, 42u);
EXPECT_EQ(tester.service().calls(), 1u);
}
To drive a hand-written pw::async2::Task, pass it to
RunToCompletion() as an lvalue. The task stays in scope, so you can inspect
its results afterwards. Here, EchoUnaryClientTask is a task that makes one
EchoUnary call and stores the response:
TEST(TestHarnessTest, UnaryTask) {
TestHarness<EchoFutureService> tester;
EchoUnaryClientTask task(tester.service_client(), 21);
tester.RunToCompletion(task);
PW_TEST_ASSERT_OK(task.result());
EXPECT_EQ(task.result()->val, 42u);
}
When an RPC must pause on an external event or another stream operation before
completing, use RunInTaskUntilStalled() to advance the call and inspect its
intermediate state:
TEST(TestHarnessTest, StalledCallResumedWhenStreamFinishes) {
TestHarness<EchoFutureService> tester;
auto call =
tester.RunToCompletion(tester.service_client().AccumulateClientStream());
PW_TEST_ASSERT_OK(call);
auto& response_fut = call->response();
// First run stalls because the client has not finished the request stream.
EXPECT_TRUE(tester.RunInTaskUntilStalled(response_fut).IsPending());
PW_TEST_EXPECT_OK(
tester.RunToCompletion(call->writer().Write(EchoRequest{.val = 42})));
PW_TEST_EXPECT_OK(tester.RunToCompletion(call->writer().Finish()));
auto poll = tester.RunInTaskUntilStalled(response_fut);
ASSERT_TRUE(poll.IsReady());
PW_TEST_ASSERT_OK(*poll);
EXPECT_EQ((*poll)->val, 42u);
}