🔷 Unit Testing CoreBluetooth: Part 1

A case study of how I approach unit testing of a CoreBluetooth layer in an iOS application.

Kewin
Kewin
Jan 24 · 5 min read

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 stateproperties, 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.

The Startup

Get smarter at building your thing. Join The Startup’s +787K followers.

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Kewin

Written by

Kewin

Lead iOS Engineer @ Jabra 🇩🇰 Full time 💻📲 — part time 🚴🏽‍⛷🏃🏽‍🏊🏼‍

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +787K followers.

Kewin

Written by

Kewin

Lead iOS Engineer @ Jabra 🇩🇰 Full time 💻📲 — part time 🚴🏽‍⛷🏃🏽‍🏊🏼‍

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +787K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store