All the most important AbleLib classes - AbleManager
, AbleComm
, AbleDeviceStorage
, AbleGattServer
and sockets - are actually implementations of interfaces/protocols - IAbleManager
, IAbleComm
, IAbleDeviceStorage
, IAbleGattServer
, etc.. This allows for easy mocking when testing and plays nicely with dependency injection/resolution. Instead of using AbleManager.shared
(which creates AbleComm
instances) or AbleDeviceStorage.default
, you can provide your own mocked implementations for testing purposes and easily tie them into your code. You can even mock or substitute some of those key implementations, and have the AbleLib ecosystem keep working as if nothing happened.
Let's assume you're using Koin. Instead of using AbleManager.shared
, define the IAbleManager
providers like this:
val myModule = module {
single<IAbleManager> { AbleManager.shared }
}
val mockModule = module {
single<IAbleManager> {
object : IAbleManager() {
// implement desired mocked functionality
}
}
}
Then, use the injected instance where necessary:
val ableManager: IAbleManager by inject()
...
ableManager.scan { result ->
...
}
You can implement a custom IAbleComm
to simulate, mock or alter some or all parts of the peripheral communication. You can then inject this custom implementation into the AbleLib ecosystem by implementing a custom IAbleManager
and providing instances via its comm
method.
Mocking comm is a three-step process:
IAbleManager
.IAbleComm
.IAbleManager.comm
method.If you plan on mocking comm, make sure not to use ableDevice.comm
(or asyncComm
) to when instantiating comm, as these always return the default SDK implementation (AbleComm
). Instead, always use ableManager.comm
.
class MockComm(val params: AbleCommParams,
override val callback: AbleCommCallback
) : IAbleComm {
...
}
class MockAbleManager : IAbleManager {
...
override fun comm(params: AbleCommParams, callback: AbleCommCallback): IAbleComm {
return MockComm(params, callback)
}
...
}
Then, later in your code, use it like this:
val ableManager: IAbleManager by inject()
...
val device = ...
val comm = AbleCoroutineComm(ableManager, AbleCommParams(device))
val asyncComm = AbleCommBuilder(ableManager, device)
Mocking a GATT server is trivial - simply create your own implementation of IAbleGattServer
/IAbleGATTServer
and have your custom implementation of IAbleManager
return it in its startGattServer
/startGATTServer
.
Mocking sockets is even easier, since they aren't tied to other parts of the AbleLib ecosystem. YOu just need to make your mock implementations of IAbleSocketConnection
, and have your mocked IAbleSocket
s and/or IAbleServerSocket
s use it.
Some convenience methods or extensions internally use AbleManager.shared
, so please be mindful not to use them if you plan on using your own implementation of IAbleManager
. Here's an exhaustive list of these methods for each platform:
AbleDevice.pair()
.AbleDevice.pair(callback)
.AbleDevice.comm()
.AbleDevice.asyncComm()
.AbleComm
generally uses AbleManager.shared
for a lot of state tracking and internal actions.AbleDeviceStorage.default.importDevices(deviceList)
.AbleService
has an overridable field manager
that, by default, points to AbleManager.shared
.