| # Understanding `bt-host` unit tests | |
| This guide explains how to write tests for Fuchsia's | |
| [`bt-host` driver](/src/connectivity/bluetooth/core/bt-host/) by detailing the | |
| [`SMP_Phase1Test`](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/sm/phase_1_unittest.cc#36) | |
| test fixture and the | |
| [FeatureExchangeBothSupportSCFeaturesHaveSC](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/sm/phase_1_unittest.cc#318) | |
| test case. | |
| The `bt-host` driver implements most of the Bluetooth Core Specification v5.2, | |
| Volume 3 (Host Subsystem). | |
| The Fuchsia project places a | |
| [strong emphasis on automated testing](/docs/development/testing/testability_rubric.md) | |
| and the Bluetooth team strives to be | |
| [leaders of testing culture](/src/connectivity/bluetooth/README.md#tests). | |
| In order to merge your `bt-host` driver change in the Fuchsia source tree, you | |
| need to write automated tests for that change. | |
| ## Introduction | |
| `bt-host` is a relatively large codebase with many layers of abstraction. | |
| Below is a diagram of the `bt-host` driver's main logical components: | |
|  {#architecture-diagram} | |
| Every abstraction layer in the graph roughly corresponds to an entire protocol. | |
| When working with `bt-host` tests, focus on understanding the relationship | |
| between the layer you're currently working on (e.g. SM), and the layer(s) | |
| directly beneath it (e.g. L2CAP). | |
| While each layer may have additional internal layers of abstraction, these | |
| inter-protocol relationships are most frequently mocked and/or exercised in | |
| tests. | |
| ### Resources | |
| `bt-host` is written in C++, and Fuchsia uses the | |
| [gUnit](/docs/development/languages/c-cpp/library_restrictions.md#third_partygoogletest) | |
| [Googletest](https://github.com/google/googletest) library for C++ tests. | |
| To work with `bt-host` unit tests, you need a solid understanding of the | |
| following resources: | |
| + [GTest primer](https://github.com/google/googletest/blob/HEAD/docs/primer.md) | |
| + [Test fixtures](https://github.com/google/googletest/blob/HEAD/docs/primer.md#test-fixtures-using-the-same-data-configuration-for-multiple-tests-same-data-multiple-tests) | |
| The following topics occasionally come up while writing `bt-host` tests and can | |
| be referenced as needed: | |
| + [Advanced googletest topics](https://github.com/google/googletest/blob/HEAD/docs/advanced.md) | |
| + [Assertion placement](https://github.com/google/googletest/blob/HEAD/docs/advanced.md#assertion-placement) | |
| + [Death tests](https://github.com/google/googletest/blob/HEAD/docs/advanced.md#death-tests) | |
| + [Scoped tracing](https://github.com/google/googletest/blob/HEAD/docs/advanced.md#adding-traces-to-assertions) | |
| ### `bt-host` unit test overview | |
| Most `bt-host` tests are written in the following pattern: | |
| 1. Create a [test fixture](#test_fixtures) to store | |
| [test doubles](#test_doubles) and the Layer Under Test (LUT). | |
| 1. Construct the LUT within that test fixture, using test doubles in place of | |
| the LUT's depenencies. | |
| Note: Fuchsia does not allow usage of the gMock framework, so any mock | |
| dependencies must be written without it. For more information, see | |
| [Library restrictions](/docs/development/languages/c-cpp/library_restrictions.md). | |
| 1. Exercise a functionality within the LUT. For example, this guide examines | |
| the `FeatureExchangeBothSupportSCFeaturesHaveSC` test case. | |
| 1. Validate that the higher-level command results in an expected behavior. | |
| ### Test fixtures | |
| GTest test fixtures are reusable environments that often store test | |
| dependencies and provide convenience methods for writing unit tests. The test | |
| fixture used in this example is the [`SMP_Phase1Test` class](#smp_phase1_test). | |
| ### Test doubles | |
| Test doubles are objects used to substitute for real objects in test code. There | |
| are many different types of test doubles. The two example test doubles used in | |
| `SMP_Phase1Test` are [`FakeChannel`](#fake_channel) and | |
| [`FakeListener`](#fakelistener). | |
| ## `SMP_Phase1Test` test fixture {#smp_phase1_test} | |
| [`SMP_Phase1Test`](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/sm/phase_1_unittest.cc#36) | |
| is the test fixture used to test the | |
| [`sm::Phase1`](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/sm/phase_1.h) | |
| class. | |
| The `Phase1` class is responsible for Phase 1 of Bluetooth Low Energy (BLE) | |
| pairing, in which the devices negotiate the security features of the pairing. | |
| This section annotates the test fixture setup code of `SMP_Phase1Test` as a | |
| representative example of | |
| ["Creating a test fixture" and "Constructing the LUT"](#bt-host_unit_test_overview). | |
| It may be helpful to have | |
| [phase_1_unittest.cc](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/sm/phase_1_unittest.cc#42) | |
| open while reading this section. | |
| ### `SetUp()`, `TearDown()`, and `NewPhase1()` methods | |
| The constructor of `SMP_Phase1Test` does nothing. Instead, `bt-host` test | |
| fixtures typically use the GTest `SetUp()` method to initialize the test | |
| fixture. | |
| Note: The GTest FAQs have | |
| [some subtle guidance about when to use the constructor vs. `SetUp`](https://github.com/google/googletest/blob/HEAD/docs/faq.md#should-i-use-the-constructordestructor-of-the-test-fixture-or-setupteardown-ctorvssetup), | |
| but it is a `bt-host` best practice to use `SetUp()`. | |
| ```C++ | |
| void SetUp() override { NewPhase1(); } | |
| ``` | |
| `NewPhase1` is a `protected` visibility method with defaultable parameters. | |
| `bt-host` test fixtures commonly delegate to a `New<test-fixture-name>` method | |
| with defaultable parameters from `SetUp()`. The `New`\* method does the work of | |
| setting up resources/test doubles and creating the LUT. This enables test | |
| cases[^1] to reinitialize the test fixture by calling `New`\* with different | |
| parameters. | |
| ```C++ | |
| void NewPhase1(Role role = Role::kInitiator, | |
| Phase1Args phase_args = Phase1Args(), | |
| hci::Connection::LinkType ll_type = | |
| hci::Connection::LinkType::kLE) { | |
| ``` | |
| For `NewPhase1`, the configurable aspects are: | |
| + The device role | |
| + The transport type | |
| + A struct that holds the rest of the Phase1 arguments, which makes it | |
| easier to change only one of the "default" arguments: | |
| ##### `Phase1Args` struct | |
| ```C++ | |
| struct Phase1Args { | |
| PairingRequestParams preq = PairingRequestParams(); | |
| IOCapability io_capability = IOCapability::kNoInputNoOutput; | |
| BondableMode bondable_mode = BondableMode::Bondable; | |
| SecurityLevel level = SecurityLevel::kEncrypted; | |
| bool sc_supported = false; | |
| }; | |
| ``` | |
| ### L2CAP mock dependency {#fake_channel} | |
| `L2CAP` channels provide a logical connection to a peer protocol/service, and | |
| are depended on by higher-level protocols like `ATT`, `GATT`, `SMP`, `SDP`. | |
| `FakeChannel` is used as a mock dependency to test how real objects send and | |
| receive messages over | |
| [L2CAP channels](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/l2cap/channel.h). | |
| The first test double created in `NewPhase1` is a `FakeChannel` mock object: | |
| ```C++ | |
| uint16_t mtu = | |
| phase_args.sc_supported ? l2cap::kMaxMTU : kNoSecureConnectionsMtu; | |
| ChannelOptions options(cid, mtu); | |
| options.link_type = ll_type; | |
| ... | |
| fake_chan_ = CreateFakeChannel(options); | |
| sm_chan_ = std::make_unique<PairingChannel>(fake_chan_); | |
| ``` | |
| The `CreateFakeChannel` method is available because `SMP_Phase1Test` inherits | |
| from | |
| [`FakeChannelTest`](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/l2cap/fake_channel_test.h).[^2] | |
| ### FakeListener | |
| In real code, `PairingPhase` uses `PairingPhase::Listener` to communicate with | |
| the higher-level `SecurityManager` class. `FakeListener` provides a mock of this | |
| dependency for testing. | |
| ```C++ | |
| listener_ = std::make_unique<FakeListener>(); | |
| ``` | |
| While not a protocol-level dependency, `FakeListener` exemplifies another common | |
| `bt-host` test pattern. Classes often take interface pointers to communicate | |
| with layers above them. Test doubles implementing these interfaces are passed to | |
| the LUT to verify that the LUT communicates correctly with the layer above it. | |
| Note: For another example of this pattern, see | |
| [`BrEdrConnectionManager`'s `PairingDelegate` pointer](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/gap/bredr_connection_manager.h#106) | |
| and | |
| [how it's mocked out in tests](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/gap/bredr_connection_manager_unittest.cc#914). | |
| ### Completion callback | |
| `Phase1` stores a callback parameter. When `Phase1` completes, it returns the | |
| results of `Phase1` through this callback. `complete_cb` is used as this | |
| callback when instantiating `Phase1`. | |
| `complete_cb` stores the results of `Phase1` (in this case the `features`, | |
| `preq`, and `pres` arguments) into test fixture variables (`features_`, | |
| `last_pairing_req_`, and `last_pairing_res_`) so that test cases can check that | |
| these variables are generated correctly. | |
| ```C++ | |
| auto complete_cb = [this](PairingFeatures features, | |
| PairingRequestParams preq, | |
| PairingResponseParams pres) { | |
| feature_exchange_count_++; | |
| features_ = features; | |
| last_pairing_req_ = util::NewPdu(sizeof(PairingRequestParams)); | |
| last_pairing_res_ = util::NewPdu(sizeof(PairingResponseParams)); | |
| PacketWriter preq_writer(kPairingRequest, last_pairing_req_.get()); | |
| PacketWriter pres_writer(kPairingResponse, last_pairing_res_.get()); | |
| *preq_writer.mutable_payload<PairingRequestParams>() = preq; | |
| *pres_writer.mutable_payload<PairingResponseParams>() = pres; | |
| }; | |
| ``` | |
| ### LUT Instantiation | |
| The next step is to create the `Phase1` LUT according to the `NewPhase1` | |
| parameters. The LUT is stored in the test fixture's `phase_1_` variable. | |
| ```C++ | |
| if (role == Role::kInitiator) { | |
| phase_1_ = Phase1::CreatePhase1Initiator( | |
| sm_chan_->GetWeakPtr(), listener_->as_weak_ptr(), | |
| phase_args.io_capability, phase_args.bondable_mode, | |
| phase_args.level, std::move(complete_cb)); | |
| } else { | |
| phase_1_ = Phase1::CreatePhase1Responder( | |
| sm_chan_->GetWeakPtr(), listener_->as_weak_ptr(), | |
| phase_args.preq, phase_args.io_capability, | |
| phase_args.bondable_mode, phase_args.level, | |
| std::move(complete_cb)); | |
| } | |
| ``` | |
| ### Remaining methods | |
| The rest of `Phase1` methods are trivial get methods to do the following: | |
| + Expose | |
| [test doubles for manipulation](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/sm/phase_1_unittest.cc#80). | |
| + Check `Phase1`'s | |
| [result output](https://fuchsia.googlesource.com/fuchsia/+/2ee647a648d93300d49328db8ef4bb0426e36c53/src/connectivity/bluetooth/core/bt-host/sm/phase_1_unittest.cc#85) | |
| against test expectations. | |
| ## `FeatureExchangeBothSupportSCFeaturesHaveSC` Test case | |
| This test case verifies that if both devices involved in pairing support a | |
| feature, in this case the Secure Connections (SC) feature, the `PairingFeatures` | |
| returned by `Phase1`'s complete callback correctly reports this. | |
| The default `NewPhase1` parameters | |
| [don't support Secure Connections](#phase1args_struct), so the code sets just | |
| the SC field of `Phase1Args` and leaves the rest defaulted for `NewPhase1`: | |
| ```C++ | |
| Phase1Args args; | |
| args.sc_supported = true; | |
| NewPhase1(Role::kInitiator, args); | |
| ``` | |
| The L2CAP messages used in this test case are written out, with the feature bit | |
| under test (`kSC`) set: | |
| ```C++ | |
| const auto kRequest = StaticByteBuffer( | |
| // [...omitted] | |
| AuthReq::kSC | AuthReq::kBondingFlag, | |
| // [...omitted] | |
| ); | |
| const auto kResponse = StaticByteBuffer( | |
| // [...omitted] | |
| AuthReq::kSC | AuthReq::kBondingFlag, | |
| // [...omitted] | |
| ); | |
| ``` | |
| Parts of `bt-host` run on asynchronous task dispatchers. In this case, | |
| `FakeChannelTest` runs its `FakeChannel` on a dispatcher. `Phase1::Start`, which | |
| performs the work of `Phase1`, also needs needs run on this dispatcher. | |
| `PostTask` puts the `Start` method onto the dispatcher. | |
| `FakeChannelTest::Expect` then runs the dispatcher and check that the next | |
| message `Phase1` sends to L2CAP is `kRequest`: | |
| ```C++ | |
| // Initiate the request in a loop task for Expect to detect it. | |
| async::PostTask(dispatcher(), [this] { phase_1()->Start(); }); | |
| ASSERT_TRUE(Expect(kRequest)); | |
| ``` | |
| The `fake_chan` is used to emulate receiving a response from the peer, which | |
| completes the Phase 1 feature exchange. In this case, the code explicitly runs | |
| the task dispatcher loop by calling `RunLoopUntilIdle()`, whereas | |
| `FakeChannelTest::Expect` did that internally: | |
| ```C++ | |
| fake_chan()->Receive(kResponse); | |
| RunLoopUntilIdle(); | |
| ``` | |
| Finally, the code verifies that: | |
| + `Phase1` does not notify `FakeListener` of an error | |
| + All expected parameters are passed up in `Phase1`'s complete callback. | |
| ```C++ | |
| EXPECT_EQ(0, listener()->pairing_error_count()); | |
| EXPECT_EQ(1, feature_exchange_count()); | |
| EXPECT_TRUE(features().initiator); | |
| EXPECT_TRUE(features().secure_connections); | |
| ASSERT_TRUE(last_preq()); | |
| ASSERT_TRUE(last_pres()); | |
| EXPECT_TRUE(ContainersEqual(kRequest, *last_preq())); | |
| EXPECT_TRUE(ContainersEqual(kResponse, *last_pres())); | |
| ``` | |
| <!-- Footnotes themselves at the bottom. --> | |
| ## Notes | |
| [^1]: | |
| A test case is actually a subclass of the test fixture | |
| [^2]: | |
| PairingChannel is an SM-specific wrapper that is not relevant to the | |
| functioning of these tests. |