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.

Anton Rutkevich

November 20, 2016
Tweet

More Decks by Anton Rutkevich

Other Decks in Programming

Transcript

  1. 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!
  2. The two major issues • Managing system complexity -> Rich

    Hickey “Simple Made Easy” • Testing the system -> this talk
  3. A non-pure function fun nextItemDescription(prefix: String): String { GLOBAL_VARIABLE++ return

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

    return "$prefix: $itemIndex" } Pure function Inputs Outputs
  5. 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])
  6. 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]
  7. Module inputs class Module( val title: String, // input )

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

    dependency: Explicit // dependency ) { fun doSomething() { // input val explicit = dependency.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 inputs class Module( val title: String, // input val

    dependency: Explicit // dependency ) { fun doSomething() { // input val explicit = dependency.getCurrentState() // input val implicit = Implicit.getCurrentState() // input // ... } }
  11. 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]
  12. Module outputs class Module( ) { var state = "Some

    state" fun doSomething() { state = "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 // ... } }
  14. 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 // ... } }
  15. 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 // ... } }
  16. 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
  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[N])
  18. Practical aspects 1. Understand explicits 2. Locate implicits and convert

    them to explicits (DI) 3. Abstract away the platform 4. Mock dependencies in tests
  19. Explicit inputs class ModuleInputs( input: String, inputLambda: () -> String,

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

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

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

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

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

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

    ) { val outputObservable = Observable.just("Output") fun getOutput(): String = "Output" init { outputLambda("Output") dependency.passOutput("Output") } }
  26. #3: Time class Module { private val nowTime = System.currentTimeMillis()

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

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

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

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

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

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

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

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

    = SimpleDateFormat("yyyy-MM-dd HH:mm") .format(timestamp) } fun test() { val timestamp = 1479237994L }
  35. #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) }
  36. #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) }
  37. #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 }
  38. #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 }
  39. #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 }
  40. 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
  41. Do not expose Platform API @Module class PlatformModule(private val context:

    Context) { @Provides @Singleton fun providePlatformWrapper(): PlatformWrapper = PlatformWrapper(context) }
  42. 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 }
  43. 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 }
  44. Platform entry points Should also be as stupid as possible

    Activity Service Broadcast receiver Content provider
  45. Interfaces still work everywhere interface MyService { fun doSomething() class

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

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

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

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

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

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

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

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

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

    through DI) • Gain control over initialization process • You can cover Models with tests!