瞭解 Bt-host 單元測試

本指南說明如何為 Fuchsia 的 bt-host 驅動程式,具體說明 SMP_Phase1Test。 測試韌體 FeatureExchangeBothSupportSCFeaturesHaveSC 測試案例

bt-host 驅動程式庫實作了多數 Bluetooth Core 規格 v5.2、 磁碟區 3 (主機子系統)。

Fuchsia 專案提出了 高度重視自動化測試 藍牙團隊也致力於 測試文化的領導人

如要合併 Fuchsia 來源樹狀結構中的 bt-host 驅動程式庫變更,您必須 為這些變更編寫自動化測試

簡介

bt-host 是相對較大的程式碼集,具有許多抽象層。

以下是 bt-host 驅動程式庫的主要邏輯元件的圖表:

bt 主機主邏輯元件的圖表 {#architecture-diagram}

圖表中的每個抽象層大致對應整個通訊協定。

處理 bt-host 測試時,請著重瞭解彼此之間的關係 但可在現行層 (例如 SM) 與對應層之間進行 放在正下方 (例如 L2CAP)。

雖然每個資料層可能都有額外的內部抽象層 通訊協定間關係最常模擬和/或練習 測試。

資源

bt-host 是以 C++ 編寫,而 Fuchsia 使用 gUnit 適用於 C++ 測試的 Googletest 程式庫。

如要進行 bt-host 單元測試,必須充分瞭解 下列資源:

設計 bt-host 測試時,偶爾會出現下列主題, 需要參考:

bt-host 單元測試總覽

大多數的 bt-host 測試都是以下列模式編寫:

  1. 建立測試固件來儲存資料 「test Doubles」和「Layer Under Test」(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。這麼做可啟用測試 案件1,使用不同的值呼叫 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 來確認 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) 功能,PairingFeatures Phase1 的完整回呼所傳回的正確回報方式。

預設的 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 的工作,也需要在這個調度器上執行。

PostTask 會將 Start 方法放入調度工具。 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 專用的包裝函式,與 這些測試的運作方式