From ff01cd4c6f590f5d76f1b6e6713bf315584e3a02 Mon Sep 17 00:00:00 2001 From: dbolduc Date: Fri, 1 Jul 2022 23:50:31 -0400 Subject: [PATCH] doc(bigtable): how to mock the Data API --- google/cloud/bigtable/doc/bigtable-main.dox | 2 + .../cloud/bigtable/doc/bigtable-mocking.dox | 53 ++++++++ google/cloud/bigtable/examples/CMakeLists.txt | 5 +- .../examples/bigtable_examples_unit_tests.bzl | 1 + .../bigtable/examples/howto_mock_data_api.cc | 121 ++++++++++++++++++ .../bigtable/mocks/mock_data_connection.h | 2 + 6 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 google/cloud/bigtable/doc/bigtable-mocking.dox create mode 100644 google/cloud/bigtable/examples/howto_mock_data_api.cc diff --git a/google/cloud/bigtable/doc/bigtable-main.dox b/google/cloud/bigtable/doc/bigtable-main.dox index 83cf7fb36ea69..b933f643f8abf 100644 --- a/google/cloud/bigtable/doc/bigtable-main.dox +++ b/google/cloud/bigtable/doc/bigtable-main.dox @@ -88,6 +88,8 @@ which should give you a taste of the Cloud Bigtable C++ client library API. - @ref bigtable-samples-grpc-credentials "Examples for using various gRPC Credential Classes with Cloud Bigtable C++ client" +- @ref bigtable-mocking "Mocking the Cloud Bigtable C++ client" + [read-more-about-gcp-bigtable]: https://cloud.google.com/bigtable/docs/ 'Read more about GCP Bigtable' [read-instances-clusters]: https://cloud.google.com/bigtable/docs/instances-clusters-nodes 'Instances and Clusters' [read-tables]: https://cloud.google.com/bigtable/docs/overview 'Tables' diff --git a/google/cloud/bigtable/doc/bigtable-mocking.dox b/google/cloud/bigtable/doc/bigtable-mocking.dox new file mode 100644 index 0000000000000..21100a3e201e9 --- /dev/null +++ b/google/cloud/bigtable/doc/bigtable-mocking.dox @@ -0,0 +1,53 @@ +/*! + +@page bigtable-mocking 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. + +@snippet howto_mock_data_api.cc required-includes + +The example uses a number of aliases to save typing and improve readability: + +@snippet howto_mock_data_api.cc helper-aliases + +Create a mock connection: + +@snippet howto_mock_data_api.cc create-mock + +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. + +@snippet howto_mock_data_api.cc simulate-call + +Create a table with the mocked connection: + +@snippet howto_mock_data_api.cc create-table + +Make the table call: + +@snippet howto_mock_data_api.cc make-call + +To verify the results, we loop over the rows returned by the `RowReader`: + +@snippet howto_mock_data_api.cc verify-results + +## 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. + +@snippet howto_mock_data_api.cc all + +*/ diff --git a/google/cloud/bigtable/examples/CMakeLists.txt b/google/cloud/bigtable/examples/CMakeLists.txt index 3343e157927f1..949b0c726f440 100644 --- a/google/cloud/bigtable/examples/CMakeLists.txt +++ b/google/cloud/bigtable/examples/CMakeLists.txt @@ -47,8 +47,9 @@ if (BUILD_TESTING) table_admin_iam_policy_snippets.cc table_admin_snippets.cc) - set(bigtable_examples_unit_tests # cmake-format: sort - bigtable_examples_common_test.cc) + set(bigtable_examples_unit_tests + # cmake-format: sort + bigtable_examples_common_test.cc howto_mock_data_api.cc) include(CreateBazelConfig) export_list_to_bazel("bigtable_examples.bzl" "bigtable_examples" YEAR diff --git a/google/cloud/bigtable/examples/bigtable_examples_unit_tests.bzl b/google/cloud/bigtable/examples/bigtable_examples_unit_tests.bzl index a64f3b9bb298a..5547d4d10b09a 100644 --- a/google/cloud/bigtable/examples/bigtable_examples_unit_tests.bzl +++ b/google/cloud/bigtable/examples/bigtable_examples_unit_tests.bzl @@ -18,4 +18,5 @@ bigtable_examples_unit_tests = [ "bigtable_examples_common_test.cc", + "howto_mock_data_api.cc", ] diff --git a/google/cloud/bigtable/examples/howto_mock_data_api.cc b/google/cloud/bigtable/examples/howto_mock_data_api.cc new file mode 100644 index 0000000000000..7d120881e97ca --- /dev/null +++ b/google/cloud/bigtable/examples/howto_mock_data_api.cc @@ -0,0 +1,121 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! [all] + +//! [required-includes] +#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 +//! [required-includes] + +namespace { + +//! [helper-aliases] +using ::testing::ByMove; +using ::testing::ElementsAre; +using ::testing::Return; +namespace gc = ::google::cloud; +namespace cbt = ::google::cloud::bigtable; +namespace cbtm = ::google::cloud::bigtable_mocks; +//! [helper-aliases] + +TEST(MockTableTest, ReadRowsSuccess) { + // Create a mock connection: + //! [create-mock] + auto mock = std::make_shared(); + //! [create-mock] + + // Set up our mock connection to return a `RowReader` that will successfully + // yield "r1" then "r2": + //! [simulate-call] + std::vector rows = {cbt::Row("r1", {}), cbt::Row("r2", {})}; + EXPECT_CALL(*mock, ReadRows) + .WillOnce(Return(ByMove(cbtm::MakeRowReader(rows)))); + //! [simulate-call] + + // Create a table with the mocked connection: + //! [create-table] + cbt::Table table(mock, cbt::TableResource("project", "instance", "table")); + //! [create-table] + + // Make the table call: + //! [make-call] + auto reader = table.ReadRows(cbt::RowSet(), cbt::Filter::PassAllFilter()); + //! [make-call] + + // Loop over the rows returned by the `RowReader` and verify the results: + //! [verify-results] + std::vector row_keys; + for (gc::StatusOr const& row : reader) { + ASSERT_TRUE(row.ok()); + row_keys.push_back(row->row_key()); + } + EXPECT_THAT(row_keys, ElementsAre("r1", "r2")); + //! [verify-results] +} + +TEST(MockTableTest, ReadRowsFailure) { + auto mock = std::make_shared(); + + // Return a `RowReader` that yields only a failing status (no rows). + gc::Status final_status(gc::StatusCode::kPermissionDenied, "fail"); + EXPECT_CALL(*mock, ReadRows) + .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`, 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(); + 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`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 cal. + table.AsyncReadRows(on_row, on_finish, cbt::RowSet(), + cbt::Filter::PassAllFilter()); +} + +} // namespace + +//! [all]l diff --git a/google/cloud/bigtable/mocks/mock_data_connection.h b/google/cloud/bigtable/mocks/mock_data_connection.h index 3ce7ff0cd2e00..b6aaf61880d88 100644 --- a/google/cloud/bigtable/mocks/mock_data_connection.h +++ b/google/cloud/bigtable/mocks/mock_data_connection.h @@ -31,6 +31,8 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN * including errors from a `bigtable::Table`. To do so, construct a * `bigtable::Table` with an instance of this class. Then use the Google Test * framework functions to program the behavior of this mock. + * + * See @ref bigtable-mocking for a complete example that mocks `Table` calls. */ class MockDataConnection : public bigtable::DataConnection { public: