🔷 Unit Testing CoreBluetooth: Part 1
A case study of how I approach unit testing of a CoreBluetooth layer in an iOS application.

When it comes to unit testing the part of your code that interacts directly with the Swift libraries, multiple challenges occur: Library objects are usually final with unavailable initializers. Additionally, Swift library delegate functions do not easily allow replacing the objects with mocked versions. These challenges can, however, be overcome by implementing the principles of proper architecture that I have described in the “Unit Testing In Swift” series.
If you are curious about unit testing in Swift and this is the first time you come across my articles, I would suggest that you start with the introduction to the series.
In this article, I will show how these principles can be applied to a layer of code interacting directly with the CoreBluetooth
stack, in order to allow easy unit testing. If you are more interested in the actual implementation of the unit tests rather than the principles that allow for good unit testing, feel free to jump directly to Part 2 of this case study.
Defining the problem
Let us begin by considering the code in the gist below.
What you see is a very simple BluetoothManager
class that contains an instance of the CoreBluetooth
library’s CBCentralManager
, used to discover and connect to Bluetooth peripherals.
In order to do so, the BluetoothManager
must implement the CoreBluetooth
library’s CBCentralManagerDelegate
protocol. When we take a closer look at the two implemented delegate functions, the problem becomes clear: Both functions contain vital logic that should be tested, however as we are not able to mock and initialise the CBCentralManager
and CBPeripheral
instances, we are not able to properly test the functions in a controlled environment. Furthermore, the private CBCentralManager
instance has its delegate
property set in the initialiser of the BluetoothManager
class. This is a vital line of code in order for the application to work and we would benefit a lot from unit testing it, however this is also not possible with the current implementation.
In the following sections we will take a closer look at how we can improve the code to make it completely testable.
It’s time to build a Façade!
As we have now made it clear that the problem exists due to the fact that the CBCentralManager
and CBPeripheral
classes can not be mocked, our goal must be to make it possible to mock them. This can be done by using the isolation principles of the façade and dependency injection design patterns that are further described in depth in this article. In other words, we need to define and utilise our own protocols instead of concrete types from the internal library (in this case the central manager and the peripheral).
Implementing the façade
So how do we actually do this? The approach is to clarify which properties and functions are accessed on the types that we wish to make a protocol for. In the case of the CBCentralManager
we first access the delegate
and state
properties, followed by a call to the scanForPeripherals(withServices:options:)
function and finally a call to connect(_:options:)
is made.
In the case of the CBPeripheral
we only access its name
property.
With this knowledge we can define and implement our protocols:
Notice how the CBCentralManager
and CBPeripheral
classes both conform to their respective protocols almost without requiring additional implementations. The connect(_:options:)
function has to be implemented because a cast from Peripheral
to CBPeripheral
is needed. Throwing a fatalError
in this case is safe, as you would only ever expect the real CBCentralManager
to discover CBPeripherals
.
However we are not finished yet: Simply changing the CBCentralManager
and CBPeripheral
types to CentralManager
and Peripheral
in the BluetoothManager
class would cause compile time errors as the CBCentralManagerDelegate
protocol would not be respected. Because of this, we have to make a facade for the CBCentralManagerDelegate
protocol. Wait, what? A facade of a facade? Yes, you heard me!
Notice how the CentralManagerDelegate
extends the CBCentralManagerDelegate
functions and uses a simple cast in order to hide the CoreBluetooth
complexity. While this would be ideal, this is unfortunately not possible because the CBCentralManagerDelegate
has an objective-c exposure requirement. Concrete implementations within protocol extensions can not be translated into objective-c and thus we would get another compile time error. However, I decided to include this in the article because this is indeed very possible for internal Swift libraries which do not have the objective-c exposure requirement.
With our facades being defined, we can now improve theBluetoothManager
implementation by conforming to the CentralManagerDelegate
protocol instead of the CoreBluetooth
version:
In a perfect world we would not need to implement the old CBCentralManagerDelegate
functions (the ones below the MARK
), however as described above, the bridging between objective-c and swift requires us to do this. The sharp developer would quickly realise that the CentralManagerDelegate
protocol is in fact not needed at all, however for the cases where the objective-c exposure is not required I would strongly suggest that this intermediate protocol is implemented.
With this, the main goal has been achieved: The two delegate functions can now be completely controlled. At this point we are able to fully test the functionality of the two functions, but the BluetoothManager
class is still not fully testable.
Injecting the dependencies
In order to test the delegate
property being set (as described in the beginning of the article), we need to utilise dependency injection. Simply injecting the CentralManager
into the initialiser of the BluetoothManager
helps us solve this issue:
It is now very simple to test the delegate
property being set as well as the delegate function implementations and their respective behaviour and logic.
Exactly how to approach these unit tests, implement sophisticated mocks for our new protocols and asserting the behaviour of the functions, will be covered in the next and final part of this case study.
As always, if you have any questions or comments, feel free to reach out to me by commenting on these articles. I will reply to all messages.