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