Core Bluetooth Mock

repository·main·Indexed 42 days ago

https://github.com/nordicsemi/ios-corebluetooth-mock

A Swift library that emulates Apple's Core Bluetooth framework, enabling developers to run, test, and take screenshots of Bluetooth-enabled iOS apps using the iOS Simulator. It provides mock classes prefixed with CBM (e.g., CBMCentralManager) that imitate native CB counterparts, supporting iOS 12.0+, macOS 10.14+, tvOS 12.0+, and watchOS 4.0+.

Tokens
7.4K
Snippets
17
Records
44
Agent score
51%

What's inside Core Bluetooth Mock

  1. Overview of CoreBluetoothMock

    main

    CoreBluetoothMock is a Swift library designed to test CoreBluetooth features in your application using mocks. It allows developers to:

    • Test Bluetooth LE connectivity on a simulator (where physical Bluetooth is often unavailable).
    • Simulate Bluetooth LE devices during the development phase.
    • Automate tests and UI screenshot testing reliably.

    Note: The current version supports testing Central Manager features only. Peripheral Manager functionality is planned for a future release.

  2. Understand limitations of CBMPeripheral KVO

    main
    Because CBMPeripheral is implemented as a protocol in the CoreBluetoothMock framework rather than a native class, Key-Value Observing (KVO) features are not available. If your application relies on observing property changes on a peripheral via KVO, you will need to adjust your implementation to use alternative notification or delegation patterns.
  3. Mock a real Bluetooth LE device using CBMPeripheralSpec

    main

    You can create a mock implementation of a real Bluetooth LE device by defining a CBMPeripheralSpec instance. This allows you to simulate a device that can advertise Bluetooth LE packets, support connections, and handle GATT events (such as reading characteristic values or sending notifications).

    A CBMCentralManagerMock can interact with a CBMPeripheralSpec device using the same scan and GATT request patterns that CBMCentralManagerNative uses with real hardware.

  4. How CoreBluetoothMock works

    main

    The library emulates Core Bluetooth objects to allow testing Bluetooth-enabled apps on the iOS Simulator, where native Bluetooth is not supported.

    It provides a set of classes and constants prefixed with CBM... that imitate the native CB... counterparts (e.g., CBMCentralManager imitates CBCentralManager).

    Behavioral Logic:

    • On physical iDevices: Calls to CBMCentralManager and CBMPeripheral are forwarded to their native equivalents.
    • On Simulator: A user-defined mock implementation is used instead of the native API.
  5. Mocking Bluetooth LE Devices

    main

    To create mock implementations of Bluetooth LE devices, you use CBMCentralManagerMock. The following objects are used to define the characteristics and behavior of these mock peripherals:

    • CBMPeripheralSpec: Defines the specification of a peripheral.
    • CBMPeripheralSpecDelegate: Delegate for peripheral specification.
    • CBMProximity: Handles proximity simulation.
    • CBMAdvertisementConfig: Configures advertisement data.
  6. Migrate from CoreBluetooth to CoreBluetoothMock via Direct Replacement

    main

    If you prefer not to use type aliases, you can perform a direct replacement of the framework and types:

    1. In all files using CoreBluetooth, replace import CoreBluetooth with import CoreBluetoothMock.
    2. Replace all instances of CB... type prefixes with CBM... (e.g., CBPeripheral becomes CBMPeripheral).
    import CoreBluetoothMock
    
    let centralManager = CBMCentralManager(delegate: self, queue: .main)
  7. Install CoreBluetoothMock

    main

    You can install the library using CocoaPods, Carthage, or Swift Package Manager.

    CocoaPods

    Add pod 'CoreBluetoothMock' to your Podfile and run pod install.

    Carthage

    Add the following to your Cartfile (replacing x.y with your version): github "https://github.com/nordicsemi/IOS-CoreBluetooth-Mock" ~> x.y Then run carthage update --platform iOS.

    Swift Package Manager

    In Xcode, use File -> Swift Packages -> Add package dependency with the URL https://github.com/nordicsemi/IOS-CoreBluetooth-Mock.git.

    Alternatively, if using a Swift.package file, add the dependency and target as shown below.

    # CocoaPods
    target 'YourAppTargetName' do
        pod 'CoreBluetoothMock'
    end
    
    # Carthage
    github "https://github.com/nordicsemi/IOS-CoreBluetooth-Mock" ~> x.y
    
    # Swift Package Manager
    dependencies: [
        .package(name: "CoreBluetoothMock", 
                 url: "https://github.com/nordicsemi/IOS-CoreBluetooth-Mock.git", 
                 .upToNextMajor(from: "x.y"))
    ]
    targets: [
        .target(
            name: "<Your target name>",
            dependencies: ["CoreBluetoothMock"]),
    ]
  8. Migrate from CoreBluetooth to CoreBluetoothMock using Type Aliases (Recommended)

    main

    The recommended way to migrate is to use type aliases so you don't have to rename all your existing CB... types.

    1. Add the CoreBluetoothMock dependency via Swift Package Manager, CocoaPods, or Carthage.
    2. Copy the CoreBluetoothTypeAliases.swift file from the Example app into your project. This file maps all CBM... types to CB... names.
    3. (Optional) You can remove import CoreBluetooth from your files, as the local type aliases will take precedence, but it is not strictly required.
    // After adding CoreBluetoothTypeAliases.swift, your existing code remains unchanged:
    let centralManager = CBCentralManager(delegate: self, queue: .main)
  9. How to define and simulate mock Bluetooth LE peripherals

    main

    To test Bluetooth functionality without physical hardware, use CBMCentralManagerMock to interact with CBMPeripheralSpec objects.

    1. Define the specification: Use CBMPeripheralSpec.simulatePeripheral(identifier:proximity:) to get a CBMPeripheralSpec.Builder. Use this builder to configure advertising data, connection parameters, and a delegate to handle logic.
    2. Register the peripheral: Add your built specification to the mock manager using CBMCentralManagerMock.simulatePeripherals(_:). Once added, the peripheral begins advertising and becomes discoverable via scanning.
    let blinky = CBMPeripheralSpec
        .simulatePeripheral(proximity: .near)
        .advertising(
            advertisementData: [
                CBMAdvertisementDataLocalNameKey    : "nRF Blinky",
                CBMAdvertisementDataServiceUUIDsKey : [CBMUUID.nordicBlinkyService],
                CBMAdvertisementDataIsConnectable   : true as NSNumber
            ],
            withInterval: 0.250)
        .connectable(
            name: "nRF Blinky",
            services: [.blinkyService],
            delegate: BlinkyCBMPeripheralSpecDelegate(),
            connectionInterval: 0.045,
            mtu: 23)
        .build()
    
    // Register the mock peripheral to start simulation
    CBMCentralManagerMock.simulatePeripherals([blinky])
  10. Initialize a CBMCentralManager using CBMCentralManagerFactory

    main

    To ensure you are using the correct implementation (either CBMCentralManagerNative or CBMCentralManagerMock), do not instantiate CBMCentralManager directly. Instead, use the CBMCentralManagerFactory.initiate(delegate:queue:) method. This factory pattern abstracts the choice between the real Core Bluetooth implementation and the mock implementation.

    let manager = CBMCentralManagerFactory.initiate(delegate: self, queue: ...)
  11. Register for Connection Events

    main

    To monitor specific connection-related lifecycle events, implement the following method:

    • centralManager(_:connectionEventDidOccur:for:): Allows the application to react to specific connection events occurring for a given peripheral.
    func centralManager(_ central: CBMCentralManager, connectionEventDidOccur event: CBMCentralManagerConnectionEvent, for peripheral: CBMPeripheral) {
        // Handle specific connection event
    }
  12. Initialize CBMCentralManager using CBMCentralManagerFactory

    main

    You cannot use the standard initializer for CBMCentralManager. Instead, use the CBMCentralManagerFactory.instance(delegate:queue:forceMock:) method.

    The forceMock parameter is critical: when set to true, it allows the mock implementation to run even on a physical device.

    // Before
    let centralManager = CBCentralManager(delegate: self, queue: .main)
    
    // After
    let centralManager = CBMCentralManagerFactory.instance(delegate: self, 
                                                         queue: .main, 
                                                         forceMock: false)