Mocking the Cloud Bigtable C++ Client with Google Mock
In this document we describe how to write unit tests that mock google::cloud::bigtable::Table
using Google Mock. This document assumes the reader is familiar with the Google Test and Google Mock frameworks and with the Cloud Bigtable C++ Client.
Mocking a successful Table::ReadRows()
First include the headers for the Table
, the mocking classes, and the Google Mock framework.
#include "google/cloud/bigtable/mocks/mock_data_connection.h" #include "google/cloud/bigtable/mocks/mock_row_reader.h" #include "google/cloud/bigtable/table.h" #include <gmock/gmock.h>
The example uses a number of aliases to save typing and improve readability:
using ::testing::ByMove; using ::testing::ElementsAre; using ::testing::Return; namespace gc = ::google::cloud; namespace cbt = ::google::cloud::bigtable; namespace cbtm = ::google::cloud::bigtable_mocks;
Create a mock connection:
auto mock = std::make_shared<cbtm::MockDataConnection>();
Now we are going to set expectations on this mock. For this test we will have it return a RowReader
that will successfully yield "r1" then "r2". A helper function, bigtable_mocks::MakeRowReader()
is provided for this purpose.
std::vector<cbt::Row> rows = {cbt::Row("r1", {}), cbt::Row("r2", {})}; EXPECT_CALL(*mock, ReadRowsFull) .WillOnce(Return(ByMove(cbtm::MakeRowReader(rows))));
Create a table with the mocked connection:
cbt::Table table(mock, cbt::TableResource("project", "instance", "table"));
Make the table call:
auto reader = table.ReadRows(cbt::RowSet(), cbt::Filter::PassAllFilter());
To verify the results, we loop over the rows returned by the RowReader
:
std::vector<cbt::RowKeyType> row_keys; for (gc::StatusOr<cbt::Row> const& row : reader) { ASSERT_TRUE(row.ok()); row_keys.push_back(row->row_key()); } EXPECT_THAT(row_keys, ElementsAre("r1", "r2"));
Full Listing
Finally we present the full code for this example in the ReadRowsSuccess
test.
We also provide ReadRowsFailure
as an example for mocking an unsuccessful Table::ReadRows()
call, plus AsyncReadRows
as an example for how one might use the DataConnection
to mock a Table::AsyncReadRows()
call.
#include "google/cloud/bigtable/mocks/mock_data_connection.h" #include "google/cloud/bigtable/mocks/mock_row_reader.h" #include "google/cloud/bigtable/table.h" #include <gmock/gmock.h> namespace { using ::testing::ByMove; using ::testing::ElementsAre; using ::testing::Return; namespace gc = ::google::cloud; namespace cbt = ::google::cloud::bigtable; namespace cbtm = ::google::cloud::bigtable_mocks; TEST(MockTableTest, ReadRowsSuccess) { // Create a mock connection: auto mock = std::make_shared<cbtm::MockDataConnection>(); // Set up our mock connection to return a `RowReader` that will successfully // yield "r1" then "r2": std::vector<cbt::Row> rows = {cbt::Row("r1", {}), cbt::Row("r2", {})}; EXPECT_CALL(*mock, ReadRowsFull) .WillOnce(Return(ByMove(cbtm::MakeRowReader(rows)))); // Create a table with the mocked connection: cbt::Table table(mock, cbt::TableResource("project", "instance", "table")); // Make the table call: auto reader = table.ReadRows(cbt::RowSet(), cbt::Filter::PassAllFilter()); // Loop over the rows returned by the `RowReader` and verify the results: std::vector<cbt::RowKeyType> row_keys; for (gc::StatusOr<cbt::Row> const& row : reader) { ASSERT_TRUE(row.ok()); row_keys.push_back(row->row_key()); } EXPECT_THAT(row_keys, ElementsAre("r1", "r2")); } TEST(MockTableTest, ReadRowsFailure) { auto mock = std::make_shared<cbtm::MockDataConnection>(); // Return a `RowReader` that yields only a failing status (no rows). gc::Status final_status(gc::StatusCode::kPermissionDenied, "fail"); EXPECT_CALL(*mock, ReadRowsFull) .WillOnce(Return(ByMove(cbtm::MakeRowReader({}, final_status)))); cbt::Table table(mock, cbt::TableResource("project", "instance", "table")); cbt::RowReader reader = table.ReadRows(cbt::RowSet(), cbt::Filter::PassAllFilter()); // In this test, we expect one `StatusOr<Row>`, that holds a bad status. auto it = reader.begin(); ASSERT_NE(it, reader.end()); EXPECT_FALSE((*it).ok()); ASSERT_EQ(++it, reader.end()); } TEST(TableTest, AsyncReadRows) { // Let's use an alias to ignore fields we don't care about. using ::testing::Unused; // Create a mock connection, and set its expectations. auto mock = std::make_shared<cbtm::MockDataConnection>(); EXPECT_CALL(*mock, AsyncReadRows) .WillOnce([](Unused, auto const& on_row, auto const& on_finish, Unused, Unused, Unused) { // Simulate returning two rows, "r1" and "r2", by invoking the `on_row` // callback. Verify the values of the returned `future<bool>`s. EXPECT_TRUE(on_row(cbt::Row("r1", {})).get()); EXPECT_TRUE(on_row(cbt::Row("r2", {})).get()); // Simulate a stream that ends successfully. on_finish(gc::Status()); }); // Create the table with a mocked connection. cbt::Table table(mock, cbt::TableResource("project", "instance", "table")); // These are example callbacks for demonstration purposes. Applications should // likely invoke their own callbacks when testing. auto on_row = [](cbt::Row const&) { return gc::make_ready_future(true); }; auto on_finish = [](gc::Status const&) {}; // Make the client call. table.AsyncReadRows(on_row, on_finish, cbt::RowSet(), cbt::Filter::PassAllFilter()); } } // namespace