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

Writing truly testable code

Writing truly testable code

Many developers understand why testing is so important, but not everyone is writing tests on a daily basis. One of the most common issues is that it’s hard to test a codebase that was not designed to be testable. This talk will cover how to write code that can be tested easily. We will also talk about how to introduce testing to an existing not-test-ready codebase.
While things like Android, MVP, Kotlin, Rx will be used in examples, understanding of these technologies is not strictly required. Also, the approach itself is generic enough to be used on any platform or with any language.

4d4c41832af945778a8ad5f690177b66?s=128

Anton Rutkevich

November 20, 2016
Tweet

More Decks by Anton Rutkevich

Other Decks in Programming

Transcript

  1. Writing a truly testable code Anton Rutkevich, Juno

  2. Why tests? • Codebase itself and its complexity grow ->

    chances of a mistake go up • Software gets released -> cost a of mistake goes up Fear of introducing modifications comes in!
  3. The two major issues • Managing system complexity -> Rich

    Hickey “Simple Made Easy” • Testing the system -> this talk
  4. Agenda • What is ‘truly testable’? • Testability in practice

    • How to start?
  5. What is ‘truly testable’?

  6. Function f(Arg[1], … , Arg[N]) -> (R[1], … , R[N])

  7. A non-pure function fun nextItemDescription(prefix: String): String { GLOBAL_VARIABLE++ return

    "$prefix: $GLOBAL_VARIABLE" } Non-pure function Inputs Outputs Inputs Outputs
  8. A pure function fun itemDescription(prefix: String, itemIndex: Int): String {

    return "$prefix: $itemIndex" } Pure function Inputs Outputs
  9. Non pure -> pure Inputs Outputs Non-pure Inputs Outputs Implicit

    Implicit Explicit Explicit
  10. Non pure -> pure Inputs Outputs Pure Explicit Explicit

  11. A function is pure if Arg[i] and R[i] are explicit:

    passed through parameters and returned as results f(Arg[1], … , Arg[N]) -> (R[1], … , R[N])
  12. Module M(In[1], … , In[N]) -> (Out[1], … , Out[N])

  13. Module inputs • Interactions with module API or dependencies API

    • Values passed to module through these interactions • Order of these interactions • Timings of these interactions In[1], … , In[N]
  14. Module inputs class Module( val title: String, // input )

    { }
  15. Module inputs class Module( val title: String, // input )

    { fun doSomething() { // input // ... } }
  16. Module inputs class Module( val title: String, // input val

    dependency: Explicit // dependency ) { fun doSomething() { // input val explicit = dependency.getCurrentState() // input // ... } }
  17. Module inputs class Module( val title: String, // input val

    dependency: Explicit // dependency ) { fun doSomething() { // input val explicit = dependency.getCurrentState() // input val implicit = Implicit.getCurrentState() // input // ... } }
  18. Module inputs class Module( val title: String, // input val

    dependency: Explicit // dependency ) { fun doSomething() { // input val explicit = dependency.getCurrentState() // input val implicit = Implicit.getCurrentState() // input // ... } }
  19. Module outputs • Interactions with its module API and its

    dependencies API • Values passed to through these interactions • Order of these interactions • Timings of these interactions • Modification of module state Out[1], … , Out[N]
  20. Module outputs class Module( ) { var state = "Some

    state" fun doSomething() { state = "New state" // output // ... } }
  21. Module outputs class Module( val dependency: Explicit // dependency )

    { var state = "Some state" fun doSomething() { state = "New state" // output dependency.setCurrentState("New state") // output // ... } }
  22. Module outputs class Module( val dependency: Explicit // dependency )

    { var state = "Some state" fun doSomething() { state = "New state" // output dependency.setCurrentState("New state") // output Implicit.setCurrentState("New state") // output // ... } }
  23. Module outputs class Module( val dependency: Explicit // dependency )

    { var state = "Some state" fun doSomething() { state = "New state" // output dependency.setCurrentState("New state") // output Implicit.setCurrentState("New state") // output // ... } }
  24. Testing a module A test is a call of the

    function with some inputs and validation of the outputs M(In[1], … , In[N]) -> (Out[1], … , Out[N]) given, when then
  25. A module is easy to test if In[i] and Out[i]

    are passed through module API or explicit dependencies API M(In[1], … , In[N]) -> (Out[1], … , Out[N])
  26. Inputs & outputs of a module Module Inputs Outputs Explicit

    dependency Implicit dependency
  27. Inputs & outputs of a module Module Inputs Outputs Explicit

    dependency
  28. Testability in practice

  29. Platform API 3rd party API Model Presenter UI Framework Platform

    wrappers View MVP. Overview
  30. Platform API 3rd party API Model Presenter UI Framework Platform

    wrappers View MVP. Overview
  31. Practical aspects 1. Understand explicits 2. Locate implicits and convert

    them to explicits (DI) 3. Abstract away the platform 4. Mock dependencies in tests
  32. Testability in practice. Understanding explicits

  33. Explicit inputs class ModuleInputs( input: String, ) { }

  34. Explicit inputs class ModuleInputs( input: String, inputLambda: () -> String,

    ) { }
  35. Explicit inputs class ModuleInputs( input: String, inputLambda: () -> String,

    inputObservable: Observable<String>, ) { }
  36. Explicit inputs class ModuleInputs( input: String, inputLambda: () -> String,

    inputObservable: Observable<String>, ) { fun passInput(input: String) { } }
  37. Explicit inputs class ModuleInputs( input: String, inputLambda: () -> String,

    inputObservable: Observable<String>, dependency: Explicit ) { private val someField = dependency.getInput() fun passInput(input: String) { } }
  38. Explicit inputs class ModuleInputs( input: String, inputLambda: () -> String,

    inputObservable: Observable<String>, dependency: Explicit ) { private val someField = dependency.getInput() fun passInput(input: String) { } }
  39. Explicit outputs class ModuleOutputs( ) { fun getOutput(): String =

    "Output" }
  40. Explicit outputs class ModuleOutputs( outputLambda: (String) -> Unit, ) {

    fun getOutput(): String = "Output" init { outputLambda("Output") } }
  41. Explicit outputs class ModuleOutputs( outputLambda: (String) -> Unit, ) {

    val outputObservable = Observable.just("Output") fun getOutput(): String = "Output" init { outputLambda("Output") } }
  42. Explicit outputs class ModuleOutputs( outputLambda: (String) -> Unit, dependency: Explicit

    ) { val outputObservable = Observable.just("Output") fun getOutput(): String = "Output" init { outputLambda("Output") dependency.passOutput("Output") } }
  43. Explicit outputs class ModuleOutputs( outputLambda: (String) -> Unit, dependency: Explicit

    ) { val outputObservable = Observable.just("Output") fun getOutput(): String = "Output" init { outputLambda("Output") dependency.passOutput("Output") } }
  44. Testability in practice. Top 5 Implicits

  45. #5: Statics and Singletons class Module { private val state

    = Implicit.getCurrentState() }
  46. #5: Statics and Singletons class Module(dependency: Explicit) { private val

    state = dependency.getCurrentState() }
  47. #5: Statics and Singletons class Module(dependency: Explicit) { private val

    state = dependency.getCurrentState() }
  48. #4: Random generators class Module { private val fileName =

    "some-file${Random().nextInt()}" }
  49. #4: Random generators class Module(rng: Rng) { private val fileName

    = "some-file${rng.nextInt()}" }
  50. #4: Random generators class Module(rng: Rng) { private val fileName

    = "some-file${rng.nextInt()}" }
  51. #3: Time class Module { private val nowTime = System.currentTimeMillis()

    private val nowDate = Date() // and all other time/date APIs }
  52. #3: Time class Module(time: TimeProvider) { private val nowTime =

    time.nowMillis() private val nowDate = time.nowDate() // and all other time/date APIs }
  53. #3: Time class Module(time: TimeProvider) { private val nowTime =

    time.nowMillis() private val nowDate = time.nowDate() // and all other time/date APIs }
  54. #2: RxSchedulers class Module { val outputObservable = Observable .interval(1,

    TimeUnit.SECONDS) .take(5) .debounce(5, TimeUnit.SECONDS) }
  55. #2: RxSchedulers class Module(timeScheduler: Scheduler) { val outputObservable = Observable

    .interval(1, TimeUnit.SECONDS, timeScheduler) .take(5) .debounce(5, TimeUnit.SECONDS, timeScheduler) }
  56. #2: RxSchedulers class Module(timeScheduler: Scheduler) { val outputObservable = Observable

    .interval(1, TimeUnit.SECONDS, timeScheduler) .take(5) .debounce(5, TimeUnit.SECONDS, timeScheduler) }
  57. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) }
  58. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { }
  59. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val timestamp = 1479237994L }
  60. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val timestamp = 1479237994L val expected = "" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) }
  61. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val timestamp = 1479237994L val expected = "2016-11-15 22:26" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) }
  62. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val timestamp = 1479237994L val expected = "2016-11-15 22:26" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) >> `actual` locally = "2016-11-15 22:26" // UTC+3 }
  63. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val timestamp = 1479237994L val expected = "2016-11-15 22:26" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) >> `actual` locally = "2016-11-15 22:26" // UTC+3 >> `actual` on CI = "2016-11-16 02:26" // UTC+7 }
  64. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val timestamp = 1479237994L val expected = "2016-11-15 22:26" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) >> `actual` locally = "2016-11-15 22:26" // UTC+3 >> `actual` on CI = "2016-11-16 02:26" // UTC+7 }
  65. #1: Formatting & Locales Same for 1. NumberFormat 2. Currency

    3. Locale 4. TimeZone 5. ...
  66. Testability in practice. Abstracting away the platform

  67. Platform API 3rd party API Model Presenter UI Framework Platform

    wrappers View MVP. Platform wrappers
  68. Platform API 3rd party API Model Presenter UI Framework Platform

    wrappers View MVP. Platform wrappers
  69. Platform API 3rd party API Model Presenter UI Framework Platform

    wrappers View MVP. Platform wrappers
  70. Why platform wrappers? • We have no control over Platform

    APIs -> they might be hard / impossible to mock • Poor platform API design (sometimes) • You don’t need all APIs at once
  71. Do not expose Platform API @Module class PlatformModule(private val context:

    Context) { }
  72. Do not expose Platform API @Module class PlatformModule(private val context:

    Context) { @Provides @Singleton fun providePlatformWrapper(): PlatformWrapper = PlatformWrapper(context) }
  73. Do not expose Platform API @Module class PlatformModule(private val context:

    Context) { @Provides @Singleton fun providePlatformWrapper(): PlatformWrapper = PlatformWrapper(context) // Bad idea @Provides @Singleton fun provideContext(): Context = context }
  74. Do not expose Platform API @Module class PlatformModule(private val context:

    Context) { @Provides @Singleton fun providePlatformWrapper(): PlatformWrapper = PlatformWrapper(context) // Bad idea @Provides @Singleton fun provideContext(): Context = context }
  75. Platform entry points Should also be as stupid as possible

    Activity Service Broadcast receiver Content provider
  76. Testability in practice. Mocking

  77. Interfaces still work everywhere interface MyService { fun doSomething() class

    Impl(): MyService { override fun doSomething() { /* ... */ } } }
  78. Interfaces still work everywhere interface MyService { fun doSomething() class

    Impl(): MyService { override fun doSomething() { /* ... */ } } } class TestService: MyService { override fun doSomething() { /* ... */ } }
  79. Interfaces still work everywhere interface MyService { fun doSomething() class

    Impl(): MyService { override fun doSomething() { /* ... */ } } } class TestService: MyService { override fun doSomething() { /* ... */ } } val mockService = mock<MyService>()
  80. Interfaces still work everywhere interface MyService { fun doSomething() class

    Impl(): MyService { override fun doSomething() { /* ... */ } } } class TestService: MyService { override fun doSomething() { /* ... */ } } val mockService = mock<MyService>()
  81. Kotlin alternative - open classes open class MyOpenService() { open

    fun doSomething() { /* ... */ } }
  82. Another alternative - mocking finals Mockito 2 PowerMock

  83. How to start?

  84. Start with Models Platform Wrapper Model Model Model Model Model

  85. Extracting singletons object Implicit { fun getCurrentState(): String = "State"

    } class SomeModule { init { val state = Implicit.getCurrentState() } }
  86. Extracting singletons interface StateProvider { fun getCurrentState(): String } object

    Implicit { fun getCurrentState(): String = "State" } class SomeModule { init { val state = Implicit.getCurrentState() } }
  87. Extracting singletons interface StateProvider { fun getCurrentState(): String } object

    Implicit: StateProvider { override fun getCurrentState(): String = "State" } class SomeModule { init { val state = Implicit.getCurrentState() } }
  88. Extracting singletons interface StateProvider { fun getCurrentState(): String } object

    Implicit: StateProvider { override fun getCurrentState(): String = "State" } class SomeModule(stateProvider: StateProvider) { init { val state = stateProvider.getCurrentState() } }
  89. Extracting singletons interface StateProvider { fun getCurrentState(): String } object

    Implicit: StateProvider { override fun getCurrentState(): String = "State" } class SomeModule(stateProvider: StateProvider) { init { val state = stateProvider.getCurrentState() } }
  90. As a result • Implicit dependencies (Singletons) become explicit (passed

    through DI) • Gain control over initialization process • You can cover Models with tests!
  91. And then Activity Dependency Graph View Presenter

  92. What’s next?

  93. Platform API 3rd party API Model Presenter UI Framework Platform

    wrappers View MVP. Integration tests
  94. Platform API 3rd party API Model Presenter UI Framework Platform

    wrappers View MVP. Integration tests
  95. Model Presenter Platform wrappers View MVP. Integration tests

  96. Questions? Twitter, GitHub, Facebook AntonRutkevich Blog rutkevich.com