Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to test your Android Things code in a clean way

How to test your Android Things code in a clean way

Presentation slides used at GDG Devfest Pisa 0.1 and AppDevCon NL 2018

James Coggan

March 16, 2018
Tweet

More Decks by James Coggan

Other Decks in Technology

Transcript

  1. James Coggan How to test your Android Things code in

    a clean way https://jamescoggan.com @mad_team
  2. Android vs Android Things Main app started automatically Android Things

    support Library Permissions are free (reboot required for dangerous ones)
  3. Android vs Android Things Main app started automatically Android Things

    support Library Permissions are free (reboot required for dangerous ones) Google play services, Firebase …
  4. Android vs Android Things AdMob Android Pay Full screen display

    (no status bar, notifications or navigation)
  5. Android vs Android Things AdMob Android Pay Full screen display

    (no status bar, notifications or navigation) Sign-In
  6. Android vs Android Things AdMob Android Pay Full screen display

    (no status bar, notifications or navigation) Sign-In System apps (Calendar, Telephony, Settings..)
  7. Android vs Android Things AdMob Android Pay Full screen display

    (no status bar, notifications or navigation) Sign-In System apps (Calendar, Telephony, Settings..) Display is optional
  8. Why Android Things? Kotlin :) OS and Framework Maintained by

    Google OTA updates Android community Android libraries
  9. Why Android Things? Kotlin :) OS and Framework Maintained by

    Google OTA updates Android community Android libraries Reusable code/infrastructure
  10. Getting started <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER"

    /> </intent-filter> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.IOT_LAUNCHER" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
  11. MVP

  12. private fun setupButton(){ val button = PeripheralManagerService().openGpio("BCM21") button.setDirection(Gpio.DIRECTION_IN) button.setActiveType(Gpio.ACTIVE_LOW) button.setEdgeTriggerType(Gpio.EDGE_BOTH)

    button.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { onValueChanged(gpio?.value ?: false) return true } }) }
  13. class Button { interface OnValueChangedListener { fun onValue(state: Boolean) }

    private var listener: OnValueChangedListener? = null }
  14. class Button { interface OnValueChangedListener { fun onValue(state: Boolean) }

    private var listener: OnValueChangedListener? = null fun setListener(listener: OnValueChangedListener) { } }
  15. class Button { interface OnValueChangedListener { fun onValue(state: Boolean) }

    private var listener: OnValueChangedListener? = null fun setListener(listener: OnValueChangedListener) { this.listener = listener } }
  16. class Button { … private val gpio = PeripheralManagerService().openGpio("BCM21") init

    { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) } }
  17. class Button { … private val gpio = PeripheralManagerService().openGpio("BCM21") init

    { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) } }
  18. class Button { … private val gpio = PeripheralManagerService().openGpio("BCM21") init

    { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback() } }
  19. class Button { … private val gpio = PeripheralManagerService().openGpio("BCM21") init

    { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { } }) } }
  20. class Button { … private val gpio = PeripheralManagerService().openGpio("BCM21") init

    { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { return true } }) } }
  21. class Button { … private val gpio = PeripheralManagerService().openGpio("BCM21") init

    { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onValue(gpio?.value ?: false) return true } }) } }
  22. interface Sensor<out T> { fun open() fun setListener(listener: OnStateChangeListener<T>?) fun

    close() } interface OnStateChangeListener<in T> { fun onStateChanged(state: T) }
  23. class Button { interface OnValueChangedListener { fun onValue(state: Boolean) }

    private var listener: OnValueChangedListener? = null private val gpio = PeripheralManagerService().openGpio("BCM21") init { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onValue(gpio?.value ?: false) return true } }) } }
  24. class Button : Sensor<Boolean> { interface OnValueChangedListener { fun onValue(state:

    Boolean) } private var listener: OnValueChangedListener? = null private val gpio = PeripheralManagerService().openGpio("BCM21") init { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onValue(gpio?.value ?: false) return true } }) } }
  25. class Button : Sensor<Boolean> { interface OnValueChangedListener { fun onValue(state:

    Boolean) } private var listener: OnValueChangedListener? = null private val gpio = PeripheralManagerService().openGpio("BCM21") init { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onValue(gpio?.value ?: false) return true } }) } }
  26. class Button : Sensor<Boolean> { private var listener: OnStateChangeListener<Boolean>? =

    null private val gpio = PeripheralManagerService().openGpio("BCM21") init { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onValue(gpio?.value ?: false) return true } }) } }
  27. class Button : Sensor<Boolean> { private var listener: OnStateChangeListener<Boolean>? =

    null override fun setListener(listener: OnStateChangeListener<Boolean>?) { this.listener = listener } private val gpio = PeripheralManagerService().openGpio("BCM21") init { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onValue(gpio?.value ?: false) return true } }) }
  28. class Button : Sensor<Boolean> { private val gpio = PeripheralManagerService().openGpio("BCM21")

    init { gpio.setDirection(Gpio.DIRECTION_IN) gpio.setActiveType(Gpio.ACTIVE_LOW) gpio.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onValue(gpio?.value ?: false) return true } }) } }
  29. class Button : Sensor<Boolean> { private val gpio = PeripheralManagerService().openGpio("BCM21")

    override fun open() { gpio = PeripheralManagerService().openGpio("BCM21") gpio?.setDirection(Gpio.DIRECTION_IN) gpio?.setActiveType(Gpio.ACTIVE_LOW) gpio?.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio?.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onValue(gpio?.value ?: false) return true } }) } }
  30. class Button : Sensor<Boolean> { private val gpio = PeripheralManagerService().openGpio("BCM21")

    override fun open() { gpio = PeripheralManagerService().openGpio("BCM21") gpio?.setDirection(Gpio.DIRECTION_IN) gpio?.setActiveType(Gpio.ACTIVE_LOW) gpio?.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio?.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onValue(gpio?.value ?: false) return true } }) } }
  31. class Button : Sensor<Boolean> { private val gpio = PeripheralManagerService().openGpio("BCM21")

    override fun open() { gpio = PeripheralManagerService().openGpio("BCM21") gpio?.setDirection(Gpio.DIRECTION_IN) gpio?.setActiveType(Gpio.ACTIVE_LOW) gpio?.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio?.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onStateChanged(gpio?.value ?: false) return true } }) } }
  32. class Button : Sensor<Boolean> { … override fun close() {

    listener = null gpio.close().also { gpio = null } } }
  33. class Button : Sensor<Boolean> { private var gpio: Gpio? =

    null private var listener: OnStateChangeListener<Boolean>? = null override fun open() { gpio = PeripheralManagerService().openGpio("BCM21") gpio?.setDirection(Gpio.DIRECTION_IN) gpio?.setActiveType(Gpio.ACTIVE_LOW) gpio?.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio?.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onStateChanged(gpio?.value ?: false) return true } }) } … }
  34. class Button : Sensor<Boolean> { private var gpio: Gpio? =

    null private var listener: OnStateChangeListener<Boolean>? = null override fun open() { gpio = PeripheralManagerService().openGpio("BCM21") gpio?.setDirection(Gpio.DIRECTION_IN) gpio?.setActiveType(Gpio.ACTIVE_LOW) gpio?.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio?.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onStateChanged(gpio?.value ?: false) return true } }) } … }
  35. public abstract class Gpio extends IoBase { public static final

    int ACTIVE_HIGH = 1; public static final int ACTIVE_LOW = 0; public static final int DIRECTION_IN = 0; public static final int DIRECTION_OUT_INITIALLY_HIGH = 1; public static final int DIRECTION_OUT_INITIALLY_LOW = 2; public static final int EDGE_BOTH = 3; public static final int EDGE_FALLING = 2; public static final int EDGE_NONE = 0; public static final int EDGE_RISING = 1; public Gpio() { throw new RuntimeException("Stub!"); } public abstract void close() throws IOException; public abstract void setDirection(int var1) throws IOException; public abstract void setEdgeTriggerType(int var1) throws IOException; public abstract void setActiveType(int var1) throws IOException; public abstract void setValue(boolean var1) throws IOException; public abstract boolean getValue() throws IOException; public final void registerGpioCallback(GpioCallback callback) throws IOException { throw new RuntimeException("Stub!"); } public abstract void registerGpioCallback(GpioCallback var1, Handler var2) throws IOException; public abstract void unregisterGpioCallback(GpioCallback var1); }
  36. interface GpioDevice { @Throws(IOException::class) fun open() @Throws(IOException::class) fun close() @Throws(IOException::class)

    fun setDirection(direction: Int) @Throws(IOException::class) fun setEdgeTriggerType(triggerType: Int) @Throws(IOException::class) fun setActiveType(activeType: Int) @Throws(IOException::class) fun setValue(value: Boolean) @Throws(IOException::class) fun getValue(): Boolean
  37. class GpioConfig { companion object { const val ACTIVE_HIGH =

    1 const val ACTIVE_LOW = 0 const val DIRECTION_IN = 0 const val DIRECTION_OUT_INITIALLY_HIGH = 1 const val DIRECTION_OUT_INITIALLY_LOW = 2 const val EDGE_BOTH = 3 const val EDGE_FALLING = 2 const val EDGE_NONE = 0 const val EDGE_RISING = 1 } }
  38. class Button : Sensor<Boolean> { private var gpio: Gpio? =

    null private var listener: OnStateChangeListener<Boolean>? = null override fun open() { gpio = PeripheralManagerService().openGpio("BCM21") gpio?.setDirection(Gpio.DIRECTION_IN) gpio?.setActiveType(Gpio.ACTIVE_LOW) gpio?.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio?.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onStateChanged(gpio?.value ?: false) return true } }) } … }
  39. class Button(val gpio: GpioDevice) : Sensor<Boolean> { private var gpio:

    Gpio? = null private var listener: OnStateChangeListener<Boolean>? = null override fun open() { gpio = PeripheralManagerService().openGpio("BCM21") gpio?.setDirection(Gpio.DIRECTION_IN) gpio?.setActiveType(Gpio.ACTIVE_LOW) gpio?.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio?.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onStateChanged(gpio?.value ?: false) return true } }) } … }
  40. class Button(val gpio: GpioDevice) : Sensor<Boolean> { private var gpio:

    Gpio? = null private var listener: OnStateChangeListener<Boolean>? = null override fun open() { gpio = PeripheralManagerService().openGpio("BCM21") gpio?.setDirection(Gpio.DIRECTION_IN) gpio?.setActiveType(Gpio.ACTIVE_LOW) gpio?.setEdgeTriggerType(Gpio.EDGE_BOTH) gpio?.registerGpioCallback(object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { listener?.onStateChanged(gpio?.value ?: false) return true } }) } … }
  41. Better testability of Android Things apps. The Peripheral I/O API

    now exposes interfaces instead of abstract classes, allowing local unit tests to replace these objects with mocks and stubs more easily.
  42. class ButtonTest { private lateinit var switch: Switch @Mock private

    lateinit var gpioDevice: GpioDevice @Suppress("unused") @get:Rule var mockitoRule = MockitoJUnit.rule()!! }
  43. class ButtonTest { private lateinit var switch: Switch @Mock private

    lateinit var gpioDevice: GpioDevice @Suppress("unused") @get:Rule var mockitoRule = MockitoJUnit.rule()!! @Before fun setup() { } }
  44. class ButtonTest { private lateinit var switch: Switch @Mock private

    lateinit var gpioDevice: GpioDevice @Suppress("unused") @get:Rule var mockitoRule = MockitoJUnit.rule()!! @Before fun setup() { switch = Switch(gpioDevice) } }
  45. class ButtonTest { private lateinit var switch: Switch @Mock private

    lateinit var gpioDevice: GpioDevice @Suppress("unused") @get:Rule var mockitoRule = MockitoJUnit.rule()!! @Before fun setup() { switch = Switch(gpioDevice) } @Test fun whenOpen_thenDirectionSet() { switch.open() verify(gpioDevice).setDirection(GpioConfig.DIRECTION_IN) } }
  46. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Helper
  47. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository Helper
  48. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB Helper
  49. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper
  50. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper
  51. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper
  52. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper
  53. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper
  54. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper
  55. x

  56. x

  57. adb: failed to install things/build/outputs/apk/debug/things- debug.apk: Failure [INSTALL_FAILED_MISSING_SHARED_LIBRARY: Package couldn't

    be installed in /data/app/ com.jamescoggan.things-4076B4ghPLoLrv6BwsEvPQ==: Package com.jamescoggan.things requires unavailable shared library com.google.android.things; failing!]
  58. adb: failed to install things/build/outputs/apk/debug/things- debug.apk: Failure [INSTALL_FAILED_MISSING_SHARED_LIBRARY: Package couldn't

    be installed in /data/app/ com.jamescoggan.things-4076B4ghPLoLrv6BwsEvPQ==: Package com.jamescoggan.things requires unavailable shared library com.google.android.things; failing!]
  59. x

  60. x

  61. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper
  62. x

  63. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper
  64. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper x
  65. Drivers GpioDevice interface Button Temp sensor Led Things App View

    Presenter Model Interactor Data Repository DB API Helper Mocked GpioDevice
  66. Drivers GpioDevice interface Button Temp sensor Led Things App View

    Presenter Model Interactor Data Repository DB API Helper Mocked GpioDevice
  67. Drivers GpioDevice interface Mocked GpioDevice Implements Things App Button Temp

    sensor Led Frontend View Presenter Model Interactor Data Repository DB API Helper
  68. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Things App

    Button Temp sensor Led Frontend View Presenter Model Interactor Data Repository DB API Helper
  69. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Things App

    Button Temp sensor Led Frontend View Presenter Model Interactor Data Repository DB API Helper Test Application Mocked GpioDevice
  70. abstract class FrontendApplication : Application() { lateinit var driverComponent: DriverComponent

    override fun onCreate() { super.onCreate() driverComponent = createDriverComponent() } }
  71. abstract class FrontendApplication : Application() { lateinit var driverComponent: DriverComponent

    override fun onCreate() { super.onCreate() driverComponent = createDriverComponent() } abstract fun createDriverComponent(): DriverComponent }
  72. // Things Application class ThingsApplication : FrontendApplication() { override fun

    createDriverComponent(): DriverComponent { return DaggerThingsDriverComponent.builder().build() } }
  73. // Test application class TestApplication : FrontendApplication() { override fun

    createDriverComponent(): DriverComponent { return DaggerTestingDriverComponent.builder().build() } }
  74. class MainActivityTest { @Inject lateinit var led: Actuator<@JvmSuppressWildcards Boolean> @Inject

    lateinit var button: Sensor<@JvmSuppressWildcards Boolean> @Before fun setup() { val appContext = InstrumentationRegistry.getTargetContext().applicationContext val application = (appContext as TestApplication) val testingDriverComponent = application.driverComponent as TestingDriverComponent testingDriverComponent.inject(this) // Reset mocks reset(button) reset(led) }
  75. @Test fun givenButtonValueTrue_whenActivityLaunched_thenTheLedIsTrue() { val captor = argumentCaptor<OnStateChangeListener<Boolean>>() doNothing().whenever(button).setListener(captor.capture()) activity.launchActivity(Intent())

    val listener = captor.firstValue listener.onStateChanged(true) verify(led).setValue(true) } @Test fun givenButtonValueTrue_whenActivityLaunched_thenTheLedIsFalse() { val captor = argumentCaptor<OnStateChangeListener<Boolean>>() doNothing().whenever(button).setListener(captor.capture()) activity.launchActivity(Intent()) val listener = captor.firstValue listener.onStateChanged(true) verify(led).setValue(true) }
  76. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper
  77. Drivers Hardware GpioDevice interface Gpio GpioDevice Provider Implements Button Temp

    sensor Led Things App View Presenter Model Interactor Data Repository DB API Helper