了解 bt-host 单元测试

本指南介绍如何为 Fuchsia 的 bt-host 驱动程序 SMP_Phase1Test 测试夹具和 FeatureExchangeBothSupportSCFeaturesHaveSC 测试用例。

bt-host 驱动程序实现了蓝牙核心规范 v5.2 的大部分功能, 卷 3(主机子系统)。

Fuchsia 项目 注重自动化测试 并且蓝牙团队力争 测试文化领导者

如需合并 Fuchsia 源代码树中的 bt-host 驱动程序更改,您需要 因此需要针对这项更改编写自动化测试

简介

bt-host 是一个相对较大的代码库,包含许多抽象层。

下面是 bt-host 驱动程序的主要逻辑组件示意图:

bt-host 主要逻辑组件示意图 {#architecture-diagram}

图中的每个抽象层大致对应于整个协议。

使用 bt-host 测试时,请专注于了解两者之间的关系 当前处理的层(例如 SM) (例如 L2CAP)。

虽然每一层都可能有额外的内部抽象层, 协议间关系最常被模仿和/或运用 测试。

资源

bt-host 使用 C++ 编写,Fucsia 使用 gUnit 用于 C++ 测试的 Googletest 库。

如需使用 bt-host 单元测试,您需要对 以下资源:

在编写 bt-host 测试时偶尔会出现以下主题, 可以根据需要引用:

bt-host 单元测试概览

大多数 bt-host 测试都使用以下模式编写:

  1. 创建用于存储数据的测试夹具 测试替身和被测层 (LUT)。

  2. 在该测试夹具中构建 LUT,使用测试替身代替 LUT 的依赖关系。

  3. 在 LUT 中练习功能。例如,本指南探讨的是 FeatureExchangeBothSupportSCFeaturesHaveSC 测试用例。

  4. 验证更高级别的命令是否会导致预期的行为。

测试夹具

GTest 测试夹具是可重复使用的环境,通常存储测试 依赖项,并为编写单元测试提供便捷方法。测试 本例中使用的设备是 SMP_Phase1Test

测试双打

测试替身是用于在测试代码中替换真实对象的对象。那里 是许多不同类型的测试替身。这里展示的两个测试替身示例 SMP_Phase1TestFakeChannelFakeListener

SMP_Phase1Test 测试夹具

SMP_Phase1Test 是用于测试 sm::Phase1 类。

Phase1 类负责蓝牙低功耗 (BLE) 的第 1 阶段 配对,在此过程中,设备会协商配对的安全功能。

本部分将 SMP_Phase1Test 的测试固件设置代码标注为 典型的 “创建测试夹具”和“构建 LUT”。 如果您能 phase_1_unittest.cc 打开它

SetUp()TearDown()NewPhase1() 方法

SMP_Phase1Test 的构造函数不会执行任何操作。bt-host 会测试 固件通常使用 GTest SetUp() 方法初始化测试 灯具。

void SetUp() override { NewPhase1(); }

NewPhase1 是一种带有默认参数的 protected 可见性方法。 bt-host 测试固件通常会委托给 New<test-fixture-name> 方法 (带有 SetUp() 中的默认参数)。New* 方法会执行以下任务 设置资源/测试替身并创建 LUT。这样可启用测试 case1 使用不同的方法调用 New*,以重新初始化测试固件 参数。

void NewPhase1(Role role = Role::kInitiator,
               Phase1Args phase_args = Phase1Args(),
               hci::Connection::LinkType ll_type =
                   hci::Connection::LinkType::kLE) {

对于 NewPhase1,可配置切面如下:

  • 设备角色
  • 传输类型
  • 包含剩余 Phase1 参数的结构体,使其成为 而只需更改其中一个“默认”参数:
Phase1Args 结构体
struct Phase1Args {
 PairingRequestParams preq = PairingRequestParams();
 IOCapability io_capability = IOCapability::kNoInputNoOutput;
 BondableMode bondable_mode = BondableMode::Bondable;
 SecurityLevel level = SecurityLevel::kEncrypted;
 bool sc_supported = false;
};

L2CAP 模拟依赖项

L2CAP 通道提供与对等协议/服务的逻辑连接,以及 依赖于 ATTGATTSMPSDP 等更高级别的协议。

FakeChannel 用作模拟依赖项,用于测试真实对象如何发送和 通过以下应用接收邮件: L2CAP 渠道

NewPhase1 中创建的第一个测试替身是 FakeChannel 模拟对象:

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_);

CreateFakeChannel 方法可用,因为 SMP_Phase1Test 继承了 来自 FakeChannelTest2

FakeListener

在实际代码中,PairingPhase 使用 PairingPhase::Listener 与 较高级别的 SecurityManager 类。FakeListener 对此进行了模拟, 测试依赖项

listener_ = std::make_unique<FakeListener>();

虽然不是协议级依赖项,但 FakeListener 也体现了另一种常见的 bt-host 测试图案。类通常会使用接口指针进行通信 上面是层实现这些接口的测试替身会传递到 验证 LUT 是否与上层正确通信。

完成回调

Phase1 存储一个回调参数。Phase1 完成后,它会返回 Phase1 的结果。complete_cb用作此项 在实例化 Phase1 时调用回调。

complete_cb 会存储 Phase1 的结果(在本例中为 featurespreqpres 参数)传递到测试固件变量(features_last_pairing_req_last_pairing_res_),以便测试用例可以检查 正确生成这些变量。

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 实例化

下一步是根据 NewPhase1 创建 Phase1 LUT 参数。LUT 存储在测试固件的 phase_1_ 变量中。

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));
}

其余方法

Phase1 其余方法就是用于执行以下操作的普通 get 方法:

FeatureExchangeBothSupportSCFeaturesHaveSC 测试用例

此测试用例用于验证如果配对所涉及的两个设备都支持 功能,本例中为安全连接 (SC) 功能,PairingFeaturesPhase1 的完整回调返回的正确报告此问题。

默认的 NewPhase1 参数 不支持安全连接,因此代码集 Phase1Args 的 SC 字段,并将其余字段保留为默认值 NewPhase1

Phase1Args args;
args.sc_supported = true;
NewPhase1(Role::kInitiator, args);

此测试用例中使用的 L2CAP 消息已被写出, 被测 (kSC) 集:

const auto kRequest = StaticByteBuffer(
   // [...omitted]
   AuthReq::kSC | AuthReq::kBondingFlag,
   // [...omitted]
);
const auto kResponse = StaticByteBuffer(
   // [...omitted]
   AuthReq::kSC | AuthReq::kBondingFlag,
   // [...omitted]
);

bt-host 的某些部分在异步任务调度程序上运行。在此示例中 FakeChannelTest 在调度程序上运行其 FakeChannelPhase1::Start, 执行 Phase1 的工作,也需要在此调度程序上运行。

PostTaskStart 方法放到调度程序上。 然后,FakeChannelTest::Expect 会运行调度程序,并检查下一个 Phase1发送给 L2CAP 的消息为 kRequest

// Initiate the request in a loop task for Expect to detect it.
async::PostTask(dispatcher(), [this] { phase_1()->Start(); });
ASSERT_TRUE(Expect(kRequest));

fake_chan 用于模拟接收来自对等端的响应, 完成第 1 阶段功能交换。在这种情况下,代码会 任务调度程序循环(通过调用 RunLoopUntilIdle()),而 FakeChannelTest::Expect 在内部做到了这一点:

fake_chan()->Receive(kResponse);
RunLoopUntilIdle();

最后,该代码会验证并确保:

  • Phase1 未向FakeListener通知错误
  • 所有预期参数都会在 Phase1 的完整回调中传递。
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()));

备注


  1. 测试用例实际上是测试固件的子类

  2. PairingChannel 是特定于 SM 的封装容器,与 这些测试的运作方式。