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

Writing truly testable code 2.0 - Mobius Spb 2017

Writing truly testable code 2.0 - Mobius Spb 2017

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 introducing testing to an existing not-test-ready codebase. While things like Android, MVP, Kotlin, and 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.

Anton Rutkevich

April 21, 2017
Tweet

More Decks by Anton Rutkevich

Other Decks in Programming

Transcript

  1. Why tests? • Codebase grows -> chances of making a

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

    Hickey “Simple Made Easy” • Testing the system -> this talk
  3. Agenda • Truly testable is ... • 3 rules of

    truly testable code • Testability in practice • How to start?
  4. A pure function fun itemDescription(prefix: String, itemIndex: Int): String {

    return "$prefix: $itemIndex" } Pure function Inputs Outputs
  5. A function is easy to test if Arg[i] and R[i]

    are explicit: passed through parameters and returned as results f(Arg[1], … , Arg[N]) -> (R[1], … , R[L])
  6. Module inputs class Module( val title: String, // input )

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

    dependency: Explicit // dependency ) { fun doSomething() { // input val explicit = dependency.getCurrentState() // input // ... } }
  8. 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 // ... } }
  9. 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 // ... } }
  10. Module outputs class Module( ) { var state = "Some

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

    { var state = "Some state" fun doSomething() { state = "New state" // output dependency.setCurrentState("New state") // output // ... } }
  12. 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 // ... } }
  13. 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 // ... } }
  14. Module inputs • Interactions with module API and dependencies API

    • Values passed through the interactions • Order of the interactions • Timings between the interactions In[1], … , In[N]
  15. Module outputs • Interactions with module API and dependencies API

    • Values passed through the interactions • Order of the interactions • Timings between the interactions • Modifications of the module state Out[1], … , Out[N]
  16. Testing a module Test = call of the function +

    validation of the outputs M(In[1], … , In[N]) -> (Out[1], … , Out[L]) given, when then
  17. 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[L])
  18. Explicit dependency test class Module(explicit: Explicit) { val tripled =

    3 * explicit.getValue() } @Test fun testValueGetsTripled() { }
  19. Explicit dependency test class Module(explicit: Explicit) { val tripled =

    3 * explicit.getValue() } @Test fun testValueGetsTripled() { // prepare Explicit dependency val result = Module( ??? ).tripled }
  20. Explicit dependency test class Module(explicit: Explicit) { val tripled =

    3 * explicit.getValue() } @Test fun testValueGetsTripled() { // prepare Explicit dependency val result = Module( ??? ).tripled val expected = 15 }
  21. Explicit dependency test class Module(explicit: Explicit) { val tripled =

    3 * explicit.getValue() } @Test fun testValueGetsTripled() { // prepare Explicit dependency val result = Module( ??? ).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
  22. Explicit dependency test class Module(explicit: Explicit) { val tripled =

    3 * explicit.getValue() } @Test fun testValueGetsTripled() { // prepare Explicit dependency val result = Module( ??? ).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
  23. ‘Explicit’ as a Singleton // ‘object’ stands for Singleton in

    Kotlin object Explicit { fun getValue(): Int = ... }
  24. ‘Explicit’ as a Singleton // ‘object’ stands for Singleton in

    Kotlin object Explicit { fun getValue(): Int = ... } @Test fun testValueGetsTripled() { val result = Module( ??? ).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
  25. ‘Explicit’ as a final class // Classes are final by

    default in Kotlin class Explicit { fun getValue(): Int = ... }
  26. ‘Explicit’ as a final class // Classes are final by

    default in Kotlin class Explicit { fun getValue(): Int = ... } @Test fun testValueGetsTripled() { val result = Module( ??? ).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
  27. ‘Explicit’ as an Interface interface Explicit { fun getValue(): Int

    class Impl: Explicit { override fun getValue(): Int = ... } }
  28. ‘Explicit’ as an Interface @Test fun testValueGetsTripled() { // prepare

    Explicit dependency val result = Module( ??? ).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
  29. ‘Explicit’ as an Interface @Test fun testValueGetsTripled() { val mockedExplicit

    = object : Explicit { override fun getValue(): Int = 5 } val result = Module(mockedExplicit).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
  30. ‘Explicit’ as an Interface @Test fun testValueGetsTripled() { val mockedExplicit

    = object : Explicit { override fun getValue(): Int = 5 } val result = Module(mockedExplicit).tripled val expected = 15 assertThat(result).isEqualTo(expected) }
  31. Practical aspects 1. Understand explicits 2. Locate implicits and convert

    them to explicits 3. Mock dependencies in tests 4. Abstract away the platform 5. Consider extracting implementation details
  32. Explicit inputs class ModuleInputs( input: String, inputLambda: () -> String,

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

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

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

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

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

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

    ) { val outputObservable = Observable.just("Output") fun getOutput(): String = "Output" init { outputLambda("Output") dependency.passOutput("Output") } }
  39. #3: File system & other storage class Module { fun

    initStorage(path: String) { File(path).createNewFile() } }
  40. #3: File system & other storage class Module { fun

    initStorage(path: String): FileCreationError? { return if (File(path).createNewFile()) { null } else { FileCreationError.Exists } } }
  41. #3: File system & other storage class Module { fun

    initStorage(path: String): FileCreationError? = try { if (File(path).createNewFile()) { null } else { FileCreationError.Exists } } catch (e: SecurityException) { FileCreationError.Security(e) } catch (e: Exception) { FileCreationError.Other(e) } }
  42. #3: File system & other storage class Module { fun

    initStorage(path: String): FileCreationError? = try { if (File(path).createNewFile()) { null } else { FileCreationError.Exists } } catch (e: SecurityException) { FileCreationError.Security(e) } catch (e: Exception) { FileCreationError.Other(e) } }
  43. #3: File system & other storage class Module(private val fileCreator:

    FileCreator) { fun initStorage(path: String): FileCreationError? = try { if (fileCreator.createNewFile(path)) { null } else { FileCreationError.Exists } } catch (e: SecurityException) { FileCreationError.Security(e) } catch (e: Exception) { FileCreationError.Other(e) } }
  44. #3: File system & other storage class Module(private val fileCreator:

    FileCreator) { fun initStorage(path: String): FileCreationError? = try { if (fileCreator.createNewFile(path)) { null } else { FileCreationError.Exists } } catch (e: SecurityException) { FileCreationError.Security(e) } catch (e: Exception) { FileCreationError.Other(e) } }
  45. #2: Time class Module { private val nowTime = System.currentTimeMillis()

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

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

    time.nowMillis() private val nowDate = time.nowDate() // and all other time/date APIs }
  48. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

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

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

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

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

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val mobiusConfStart = 1492758000L val expected = "2017-04-21 10:00" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) }
  53. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val mobiusConfStart = 1492758000L val expected = "2017-04-21 10:00" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) } >> `actual on dev machine` = "2017-04-21 10:00"
  54. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val mobiusConfStart = 1492758000L val expected = "2017-04-21 10:00" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) } >> `actual on dev machine` = "2017-04-21 10:00" >> `actual on CI` = "2017-04-21 07:00"
  55. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val mobiusConfStart = 1492758000L val expected = "2017-04-21 10:00" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) } >> `actual on dev machine` = "2017-04-21 10:00" // UTC +3 >> `actual on CI` = "2017-04-21 07:00" // UTC
  56. #1: Formatting & Locales class MyTimePresenter(timestamp: Long) { val formattedTimestamp

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val mobiusConfStart = 1492758000L val expected = "2017-04-21 10:00" val actual = MyTimePresenter(timestamp).formattedTimestamp assertThat(actual).isEqualTo(expected) } >> `actual on dev machine` = "2017-04-21 10:00" // UTC +3 >> `actual on CI` = "2017-04-21 07:00" // UTC
  57. Interfaces still work everywhere interface MyService { fun doSomething() class

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

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

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

    Impl(): MyService { override fun doSomething() { /* ... */ } } } class TestService: MyService { override fun doSomething() { /* ... */ } } val mockService = mock<MyService>()
  61. Extracting singletons object Implicit { fun getCurrentState(): String = "State"

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

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

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

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

    Implicit: StateProvider { override fun getCurrentState(): String = "State" } class SomeModule(stateProvider: StateProvider) { init { val state = stateProvider.getCurrentState() } }
  66. Mockito 2 for mocking final PowerMock for mocking final, static,

    singletons, … Other alternatives • Indicate design issues; can be avoided • Might blow up at some point
  67. Activity implements View interface SplashView { fun showLoading() } class

    SplashActivity: Activity { override fun onCreate() { } }
  68. Activity implements View interface SplashView { fun showLoading() } class

    SplashActivity: Activity, SplashView { override fun onCreate() { } override fun showLoading() { findViewById(R.id.progress).show() } }
  69. Activity implements View interface SplashView { fun showLoading() } class

    SplashActivity: Activity, SplashView { override fun onCreate() { SplashPresenter(view = this) } override fun showLoading() { findViewById(R.id.progress).show() } }
  70. Activity implements View interface SplashView { fun showLoading() } class

    SplashActivity: Activity, SplashView { override fun onCreate() { SplashPresenter(view = this) } override fun showLoading() { findViewById(R.id.progress).show() } }
  71. View as a separate class // Same as before class

    SplashPresenter(view: SplashView) { init { view.showLoading() } }
  72. View as a separate class interface SplashView { fun showLoading()

    class Impl : SplashView { override fun showLoading() { } } }
  73. View as a separate class interface SplashView { fun showLoading()

    class Impl(private val viewRoot: View) : SplashView { override fun showLoading() { viewRoot.findViewById(R.id.progress).show() } } }
  74. View as a separate class interface SplashView { fun showLoading()

    // Platform lives inside of Impl class Impl(private val viewRoot: View) : SplashView { override fun showLoading() { viewRoot.findViewById(R.id.progress).show() } } }
  75. View as a separate class class SplashActivity: Activity { override

    fun onCreate() { // Platform View class val rootView: View = … } }
  76. View as a separate class class SplashActivity: Activity { override

    fun onCreate() { // Platform View class val rootView: View = … SplashPresenter( view = SplashView.Impl(rootView) ) } }
  77. View as a separate class class SplashActivity: Activity { override

    fun onCreate() { // Platform View class val rootView: View = … SplashPresenter( view = SplashView.Impl(rootView) ) } }
  78. Presenter is isolated from the platform @Test fun testLoadingIsShown() {

    val mockedView = mock<SplashView>() SplashPresenter(mockedView) }
  79. Presenter is isolated from the platform @Test fun testLoadingIsShown() {

    val mockedView = mock<SplashView>() SplashPresenter(mockedView) verify(mockedView).showLoading() }
  80. Presenter is isolated from the platform @Test fun testLoadingIsShown() {

    val mockedView = mock<SplashView>() SplashPresenter(mockedView) verify(mockedView).showLoading() }
  81. They can exist in different forms • Static methods •

    Singletons • Final classes • Non-final classes
  82. They can exist in different forms • Static methods •

    Singletons • Final classes • Non-final classes
  83. They can exist in different forms • Static methods •

    Singletons • Final classes • Non-final classes
  84. They can exist in different forms • Static methods •

    Singletons • Final classes • Non-final classes
  85. They can exist in different forms • Static methods •

    Singletons • Final classes • Non-final classes
  86. Creating a wrapper interface Wrapper { fun doSomething() class Impl:

    Wrapper { override fun doSomething() { ThirdParty.doSomething() } } }
  87. Wrapper extra benefits • Fixes poor Platform API design •

    Single Responsibility instead of God objects • Easy to apply 3rd party API changes
  88. Platform API 3rd party API Model Presenter UI Framework Platform

    wrappers View MVP. Platform wrappers vs View
  89. Accessing string ids class SplashPresenter(view: SplashView, resources: Resources) { init

    { view.setTitle(resources.getString(R.string.welcome)) view.showLoading() } }
  90. Accessing string ids class SplashPresenter(view: SplashView, resources: Resources) { init

    { view.setTitle(resources.getString(R.string.welcome)) view.showLoading() } } interface Resources { fun getString(id: Int): String }
  91. Accessing string ids class SplashPresenter(view: SplashView, resources: Resources) { init

    { view.setTitle(resources.getString(R.string.welcome)) view.showLoading() } }
  92. Accessing string ids public final class R { public static

    final class string { public static final int welcome=0x7f050000; } }
  93. Implementation details class SomeModule(input: String) { val state = calculateInitialState(input)

    // Pure private fun calculateInitialState(input: String): String = "Some complex computation for $input" }
  94. Implementation details class SomeModule(input: String) { val state = calculateInitialState(input)

    // Pure private fun calculateInitialState(input: String): String = "Some complex computation for $input" }
  95. Implementation details class SomeModule(input: String) { val state = calculateInitialState(input)

    // Pure private fun calculateInitialState(input: String): String = "Some complex computation for $input" } class AnotherModule(input: String) { val state = calculateInitialState(input) // Pure private fun calculateInitialState(input: String): String = "Some complex computation for $input" }
  96. Implementation details class SomeModule(input: String) { val state = calculateInitialState(input)

    // Pure private fun calculateInitialState(input: String): String = "Some complex computation for $input" } object StateCalculator { fun calculateInitialState(input: String): String = "Some complex computation for $input" }
  97. Implementation details class SomeModule(input: String) { val state = StateCalculator.calculateInitialState(input)

    } class AnotherModule(input: String) { val state = StateCalculator.calculateInitialState(input) } object StateCalculator { fun calculateInitialState(input: String): String = "Some complex computation for $input" }
  98. Implementation details class SomeModule(input: String, stateCalculator: StateCalculator){ val state =

    stateCalculator.calculateInitialState(input) } interface StateCalculator { fun calculateInitialState(input: String): String class Impl: StateCalculator { override fun calculateInitialState(input: String): String = "Some complex computation for $input" } }
  99. As a result • Implicit dependencies (singletons) become explicit (passed

    through DI) • Initialization flow gets explicit • Models become easy to test
  100. 3 rules • Pass arguments and return results explicitly •

    Pass dependencies explicitly • Make sure you can mock dependencies
  101. Learning the art of programming, like most other disciplines, consists

    of first learning the rules and then learning when to break them. Joshua Bloch, Effective Java