Writing a truly testable code
Anton Rutkevich,
Juno
Slide 2
Slide 2 text
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!
Slide 3
Slide 3 text
The two major issues
● Managing system complexity
-> Rich Hickey “Simple Made Easy”
● Testing the system
-> this talk
Slide 4
Slide 4 text
Agenda
● What is ‘truly testable’?
● Testability in practice
● How to start?
Slide 5
Slide 5 text
What is ‘truly testable’?
Slide 6
Slide 6 text
Function
f(Arg[1], … , Arg[N]) -> (R[1], … , R[N])
Slide 7
Slide 7 text
A non-pure function
fun nextItemDescription(prefix: String): String {
GLOBAL_VARIABLE++
return "$prefix: $GLOBAL_VARIABLE"
}
Non-pure
function
Inputs Outputs
Inputs
Outputs
Slide 8
Slide 8 text
A pure function
fun itemDescription(prefix: String, itemIndex: Int): String {
return "$prefix: $itemIndex"
}
Pure
function
Inputs Outputs
Slide 9
Slide 9 text
Non pure -> pure
Inputs Outputs
Non-pure
Inputs
Outputs
Implicit
Implicit
Explicit Explicit
Slide 10
Slide 10 text
Non pure -> pure
Inputs Outputs
Pure
Explicit Explicit
Slide 11
Slide 11 text
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])
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]
Slide 14
Slide 14 text
Module inputs
class Module(
val title: String, // input
) {
}
Slide 15
Slide 15 text
Module inputs
class Module(
val title: String, // input
) {
fun doSomething() { // input
// ...
}
}
Slide 16
Slide 16 text
Module inputs
class Module(
val title: String, // input
val dependency: Explicit // dependency
) {
fun doSomething() { // input
val explicit = dependency.getCurrentState() // input
// ...
}
}
Slide 17
Slide 17 text
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
// ...
}
}
Slide 18
Slide 18 text
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
// ...
}
}
Slide 19
Slide 19 text
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]
Slide 20
Slide 20 text
Module outputs
class Module(
) {
var state = "Some state"
fun doSomething() {
state = "New state" // output
// ...
}
}
Slide 21
Slide 21 text
Module outputs
class Module(
val dependency: Explicit // dependency
) {
var state = "Some state"
fun doSomething() {
state = "New state" // output
dependency.setCurrentState("New state") // output
// ...
}
}
Slide 22
Slide 22 text
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
// ...
}
}
Slide 23
Slide 23 text
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
// ...
}
}
Slide 24
Slide 24 text
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
Slide 25
Slide 25 text
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])
Slide 26
Slide 26 text
Inputs & outputs of a module
Module
Inputs Outputs
Explicit
dependency
Implicit
dependency
Slide 27
Slide 27 text
Inputs & outputs of a module
Module
Inputs Outputs
Explicit
dependency
Slide 28
Slide 28 text
Testability in practice
Slide 29
Slide 29 text
Platform
API
3rd party
API
Model Presenter
UI
Framework
Platform wrappers
View
MVP. Overview
Slide 30
Slide 30 text
Platform
API
3rd party
API
Model Presenter
UI
Framework
Platform wrappers
View
MVP. Overview
Slide 31
Slide 31 text
Practical aspects
1. Understand explicits
2. Locate implicits and convert them to explicits (DI)
3. Abstract away the platform
4. Mock dependencies in tests
Slide 32
Slide 32 text
Testability in practice.
Understanding explicits
Slide 33
Slide 33 text
Explicit inputs
class ModuleInputs(
input: String,
) {
}
Explicit inputs
class ModuleInputs(
input: String,
inputLambda: () -> String,
inputObservable: Observable,
) {
fun passInput(input: String) { }
}
Slide 37
Slide 37 text
Explicit inputs
class ModuleInputs(
input: String,
inputLambda: () -> String,
inputObservable: Observable,
dependency: Explicit
) {
private val someField = dependency.getInput()
fun passInput(input: String) { }
}
Slide 38
Slide 38 text
Explicit inputs
class ModuleInputs(
input: String,
inputLambda: () -> String,
inputObservable: Observable,
dependency: Explicit
) {
private val someField = dependency.getInput()
fun passInput(input: String) { }
}
Slide 39
Slide 39 text
Explicit outputs
class ModuleOutputs(
) {
fun getOutput(): String = "Output"
}
Slide 40
Slide 40 text
Explicit outputs
class ModuleOutputs(
outputLambda: (String) -> Unit,
) {
fun getOutput(): String = "Output"
init {
outputLambda("Output")
}
}
Slide 41
Slide 41 text
Explicit outputs
class ModuleOutputs(
outputLambda: (String) -> Unit,
) {
val outputObservable = Observable.just("Output")
fun getOutput(): String = "Output"
init {
outputLambda("Output")
}
}
Slide 42
Slide 42 text
Explicit outputs
class ModuleOutputs(
outputLambda: (String) -> Unit,
dependency: Explicit
) {
val outputObservable = Observable.just("Output")
fun getOutput(): String = "Output"
init {
outputLambda("Output")
dependency.passOutput("Output")
}
}
Slide 43
Slide 43 text
Explicit outputs
class ModuleOutputs(
outputLambda: (String) -> Unit,
dependency: Explicit
) {
val outputObservable = Observable.just("Output")
fun getOutput(): String = "Output"
init {
outputLambda("Output")
dependency.passOutput("Output")
}
}
Slide 44
Slide 44 text
Testability in practice.
Top 5 Implicits
Slide 45
Slide 45 text
#5: Statics and Singletons
class Module {
private val state = Implicit.getCurrentState()
}
Slide 46
Slide 46 text
#5: Statics and Singletons
class Module(dependency: Explicit) {
private val state = dependency.getCurrentState()
}
Slide 47
Slide 47 text
#5: Statics and Singletons
class Module(dependency: Explicit) {
private val state = dependency.getCurrentState()
}
Slide 48
Slide 48 text
#4: Random generators
class Module {
private val fileName = "some-file${Random().nextInt()}"
}
Slide 49
Slide 49 text
#4: Random generators
class Module(rng: Rng) {
private val fileName = "some-file${rng.nextInt()}"
}
Slide 50
Slide 50 text
#4: Random generators
class Module(rng: Rng) {
private val fileName = "some-file${rng.nextInt()}"
}
Slide 51
Slide 51 text
#3: Time
class Module {
private val nowTime = System.currentTimeMillis()
private val nowDate = Date()
// and all other time/date APIs
}
Slide 52
Slide 52 text
#3: Time
class Module(time: TimeProvider) {
private val nowTime = time.nowMillis()
private val nowDate = time.nowDate()
// and all other time/date APIs
}
Slide 53
Slide 53 text
#3: Time
class Module(time: TimeProvider) {
private val nowTime = time.nowMillis()
private val nowDate = time.nowDate()
// and all other time/date APIs
}
Slide 54
Slide 54 text
#2: RxSchedulers
class Module {
val outputObservable = Observable
.interval(1, TimeUnit.SECONDS)
.take(5)
.debounce(5, TimeUnit.SECONDS)
}
Slide 55
Slide 55 text
#2: RxSchedulers
class Module(timeScheduler: Scheduler) {
val outputObservable = Observable
.interval(1, TimeUnit.SECONDS, timeScheduler)
.take(5)
.debounce(5, TimeUnit.SECONDS, timeScheduler)
}
Slide 56
Slide 56 text
#2: RxSchedulers
class Module(timeScheduler: Scheduler) {
val outputObservable = Observable
.interval(1, TimeUnit.SECONDS, timeScheduler)
.take(5)
.debounce(5, TimeUnit.SECONDS, timeScheduler)
}
Slide 57
Slide 57 text
#1: Formatting & Locales
class MyTimePresenter(timestamp: Long) {
val formattedTimestamp = SimpleDateFormat("yyyy-MM-dd HH:mm")
.format(timestamp)
}
Slide 58
Slide 58 text
#1: Formatting & Locales
class MyTimePresenter(timestamp: Long) {
val formattedTimestamp = SimpleDateFormat("yyyy-MM-dd HH:mm")
.format(timestamp)
}
fun test() {
}
Slide 59
Slide 59 text
#1: Formatting & Locales
class MyTimePresenter(timestamp: Long) {
val formattedTimestamp = SimpleDateFormat("yyyy-MM-dd HH:mm")
.format(timestamp)
}
fun test() {
val timestamp = 1479237994L
}
Slide 60
Slide 60 text
#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)
}
Slide 61
Slide 61 text
#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)
}
Slide 62
Slide 62 text
#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
}
Slide 63
Slide 63 text
#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
}
Slide 64
Slide 64 text
#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
}
Slide 65
Slide 65 text
#1: Formatting & Locales
Same for
1. NumberFormat
2. Currency
3. Locale
4. TimeZone
5. ...
Slide 66
Slide 66 text
Testability in practice.
Abstracting away the platform
Slide 67
Slide 67 text
Platform
API
3rd party
API
Model Presenter
UI
Framework
Platform wrappers
View
MVP. Platform wrappers
Slide 68
Slide 68 text
Platform
API
3rd party
API
Model Presenter
UI
Framework
Platform wrappers
View
MVP. Platform wrappers
Slide 69
Slide 69 text
Platform
API
3rd party
API
Model Presenter
UI
Framework
Platform wrappers
View
MVP. Platform wrappers
Slide 70
Slide 70 text
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
Slide 71
Slide 71 text
Do not expose Platform API
@Module
class PlatformModule(private val context: Context) {
}
Slide 72
Slide 72 text
Do not expose Platform API
@Module
class PlatformModule(private val context: Context) {
@Provides @Singleton
fun providePlatformWrapper(): PlatformWrapper =
PlatformWrapper(context)
}
Slide 73
Slide 73 text
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
}
Slide 74
Slide 74 text
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
}
Slide 75
Slide 75 text
Platform entry points
Should also be as stupid as possible
Activity
Service
Broadcast receiver
Content provider
Slide 76
Slide 76 text
Testability in practice.
Mocking
Slide 77
Slide 77 text
Interfaces still work everywhere
interface MyService {
fun doSomething()
class Impl(): MyService {
override fun doSomething() { /* ... */ }
}
}
Slide 78
Slide 78 text
Interfaces still work everywhere
interface MyService {
fun doSomething()
class Impl(): MyService {
override fun doSomething() { /* ... */ }
}
}
class TestService: MyService {
override fun doSomething() { /* ... */ }
}
Slide 79
Slide 79 text
Interfaces still work everywhere
interface MyService {
fun doSomething()
class Impl(): MyService {
override fun doSomething() { /* ... */ }
}
}
class TestService: MyService {
override fun doSomething() { /* ... */ }
}
val mockService = mock()
Slide 80
Slide 80 text
Interfaces still work everywhere
interface MyService {
fun doSomething()
class Impl(): MyService {
override fun doSomething() { /* ... */ }
}
}
class TestService: MyService {
override fun doSomething() { /* ... */ }
}
val mockService = mock()
Slide 81
Slide 81 text
Kotlin alternative - open classes
open class MyOpenService() {
open fun doSomething() { /* ... */ }
}
Slide 82
Slide 82 text
Another alternative - mocking finals
Mockito 2
PowerMock
Slide 83
Slide 83 text
How to start?
Slide 84
Slide 84 text
Start with Models
Platform Wrapper Model
Model
Model
Model
Model
Slide 85
Slide 85 text
Extracting singletons
object Implicit {
fun getCurrentState(): String = "State"
}
class SomeModule {
init {
val state = Implicit.getCurrentState()
}
}
Slide 86
Slide 86 text
Extracting singletons
interface StateProvider {
fun getCurrentState(): String
}
object Implicit {
fun getCurrentState(): String = "State"
}
class SomeModule {
init {
val state = Implicit.getCurrentState()
}
}
Slide 87
Slide 87 text
Extracting singletons
interface StateProvider {
fun getCurrentState(): String
}
object Implicit: StateProvider {
override fun getCurrentState(): String = "State"
}
class SomeModule {
init {
val state = Implicit.getCurrentState()
}
}
Slide 88
Slide 88 text
Extracting singletons
interface StateProvider {
fun getCurrentState(): String
}
object Implicit: StateProvider {
override fun getCurrentState(): String = "State"
}
class SomeModule(stateProvider: StateProvider) {
init {
val state = stateProvider.getCurrentState()
}
}
Slide 89
Slide 89 text
Extracting singletons
interface StateProvider {
fun getCurrentState(): String
}
object Implicit: StateProvider {
override fun getCurrentState(): String = "State"
}
class SomeModule(stateProvider: StateProvider) {
init {
val state = stateProvider.getCurrentState()
}
}
Slide 90
Slide 90 text
As a result
● Implicit dependencies (Singletons) become
explicit (passed through DI)
● Gain control over initialization process
● You can cover Models with tests!
Slide 91
Slide 91 text
And then
Activity
Dependency
Graph
View
Presenter
Slide 92
Slide 92 text
What’s next?
Slide 93
Slide 93 text
Platform
API
3rd party
API
Model Presenter
UI
Framework
Platform wrappers
View
MVP. Integration tests
Slide 94
Slide 94 text
Platform
API
3rd party
API
Model Presenter
UI
Framework
Platform wrappers
View
MVP. Integration tests
Slide 95
Slide 95 text
Model Presenter
Platform wrappers
View
MVP. Integration tests
Slide 96
Slide 96 text
Questions?
Twitter, GitHub, Facebook
AntonRutkevich
Blog
rutkevich.com