How to Test the BitChat BLE Transport Without Bluetooth Hardware

Yes, the BitChat BLE transport can be fully tested without Bluetooth hardware using the in-memory MockBLEService and simulated link-layer abstractions in the permissionlesstech/bitchat repository.

The permissionlesstech/bitchat repository ships a complete hardware-free test harness for its Bluetooth Low Energy mesh stack. Because the link-layer is abstracted behind BLELinkEvent and BLELinkCommand ports, you can replace the real CoreBluetooth-backed BLELinkLayer with a simulated implementation that drives the mesh engine through the exact same code paths used in production. This design makes it possible to run unit tests, integration tests, and property tests entirely in memory.

How Hardware-Free Testing Works in BitChat

The Three-Layer BLE Architecture

As documented in docs/BLE-ARCHITECTURE-V3.md, the BitChat stack splits BLE functionality into three logical layers. The link-layer interfaces with CoreBluetooth, the serial mesh engine owns all protocol state, and feature modules sit on top. The critical testing hook is that the link-layer communicates with the engine through well-defined ports—BLELinkEvent and BLELinkCommand—so swapping the real radio for a fake one does not change the engine's behavior.

The architecture introduces a simulated link-layer via components like BLEEngineManualScheduler and the conceptual SimulatedLinkLayer. When tests use these, the engine still receives BLELinkEvent instances from a simulated source and respects the same concurrency contracts—specifically bleQueue-confined work handed off to engine-confined processing—that the production app enforces.

Core Components for Testing the BitChat BLE Transport

  • MockBLEService — An in-memory BLE service that registers on a global MockBLEBus, tracks connected peers, and routes packets. It mirrors the public API of the real BLEService, including startServices(), sendMessage(), and sendPrivateMessage(), so production code runs unchanged. Defined in bitchatTests/Mocks/MockBLEService.swift.
  • MockBLEBus — A global bus that holds a registry of MockBLEService instances and an adjacency map describing which peers are "connected."
  • BLEEngineManualScheduler — A deterministic scheduler for advanced tests that need to manually step pending timers or queues. Lives in bitchat/Services/BLE/BLEEngineManualScheduler.swift.
  • Test suites — Files such as bitchatTests/BLEServiceTests.swift and bitchatTests/BLEServiceCoreTests.swift instantiate these mocks to exercise peer discovery, message flooding, private-media transfers, and noise session establishment.

Simulating Mesh Behavior Without Radios

Peer Discovery and Connection

Tests call simulateConnectedPeer(_:) or simulateConnection(with:) to establish links, and simulateDisconnectedPeer(_:) to tear them down. The bus updates its adjacency map and notifies delegates through didConnectToPeer and didUpdatePeerList, just as real Bluetooth discovery would.

Message Routing and Flooding

When a mock service calls sendMessage or sendPrivateMessage, it builds a BitchatPacket and delivers it locally (echo) while propagating it to neighbors via the bus. The mock tracks seenMessageIDs to prevent routing loops, reproducing the production mesh's flood-control and de-duplication behavior.

Deterministic Execution

Because there is no physical radio, there are no wall-clock waits. The entire mesh runs synchronously, allowing the full test suite to complete in under two seconds.

Code Examples: Testing BitChat BLE Transport in Memory

Setting Up a Two-Node Mesh

import bitchatTests

// Create a shared bus that will simulate the radio environment
let bus = MockBLEBus()

// Instantiate two mock services (they behave like real BLEService)
let alice = MockBLEService(bus: bus)
let bob   = MockBLEService(bus: bus)

// Give them human-readable nicknames (optional)
alice.mockNickname = "Alice"
bob.mockNickname   = "Bob"

// Connect the peers – this updates the adjacency map in the bus
alice.simulateConnection(with: bob)

// Attach a delegate so we can see received messages
alice.delegate = TestDelegate()
bob.delegate   = TestDelegate()

Broadcasting a Public Message

// Alice sends a public message – the bus will forward it to Bob
alice.sendMessage("Hello from Alice!")

// Both delegates receive the same BitchatMessage instance
// (the mock echoes locally before forwarding, matching production behaviour)

Sending a Private Direct Message

// Alice sends a private message to Bob's PeerID
alice.sendPrivateMessage(
    "Secret for Bob",
    to: bob.peerID,
    recipientNickname: "Bob",
    messageID: UUID().uuidString
)

// The bus delivers the packet only to Bob because they are direct neighbors.
// No other peers would see the payload.

Simulating Topology Changes

// Disconnect the peers – further messages will not be forwarded
alice.simulateDisconnectedPeer(bob.peerID)

// Re-connect later if needed
alice.simulateConnectedPeer(bob.peerID)

Advanced Timing with the Manual Scheduler

let scheduler = BLEEngineManualScheduler()
let alice = MockBLEService(bus: scheduler.bus)
let bob   = MockBLEService(bus: scheduler.bus)

// … connect, send messages, then manually step the scheduler:
scheduler.advance(by: .seconds(1))   // drives any pending timers or queues

Key Source Files for BLE Transport Testing

Summary

  • BitChat's BLE transport is fully testable without Bluetooth hardware thanks to link-layer abstraction.
  • MockBLEService and MockBLEBus provide an in-memory mesh that mirrors the production BLEService API.
  • Tests exercise identical engine code paths, concurrency contracts, and packet routing logic used by the real app.
  • The deterministic test environment runs synchronously and completes in under two seconds.
  • Advanced scenarios use BLEEngineManualScheduler to step timers and verify back-pressure behavior.

Frequently Asked Questions

Can I run integration tests for the BitChat BLE mesh on an iOS Simulator?

Yes. Because MockBLEService replaces the CoreBluetooth-dependent BLELinkLayer, you can run the full suite—including peer discovery, message flooding, and noise session establishment—on an iOS Simulator or any Swift test runner without a physical Bluetooth adapter.

Does the mock service use the same packet parsing and routing as production?

Yes. The mock builds real BitchatPacket instances and passes them through the same parsing, inbound/outbound buffers, and routing policies that the production BLEService uses. The only difference is that packets travel over MockBLEBus instead of a CoreBluetooth radio.

How does the mock prevent infinite loops during message flooding?

MockBLEService tracks seenMessageIDs and respects the same flood-control rules as the production link-layer, ensuring packets are not forwarded repeatedly to the same peers or around routing loops.

Is the hardware-free test suite fast enough for CI pipelines?

Yes. The absence of real radio initialization and wall-clock delays means the entire BLE-related test suite typically executes in less than two seconds, making it ideal for fast feedback in continuous integration environments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →