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

Droidcon Berlin: Beyond the Mockery

Droidcon Berlin: Beyond the Mockery

In software development, mocking is a popular technique used to simulate dependencies and test behaviour without relying on external systems. However, as with any technique, there are pros and cons to using mocks.

In this talk, I'll discuss why using mocks may not be the best approach and why we should instead use fakes or in-memory implementations of well-defined interfaces. We will explore the drawbacks of mocks, including how they can lead to brittle tests, slow down development, and make it difficult to refactor code.

By contrast, we will see how using fakes or in-memory implementations can provide faster feedback, increase confidence in the code, and make it easier to maintain tests as the codebase evolves. We will look at some examples of how to implement these alternatives, and how to make them useful in different testing scenarios.

Ash Davies

July 06, 2023
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

  1. Beyond the Mockery
    Ash Davies
    ashdavies.dev | [email protected]

    View Slide

  2. Legacy (adj.)
    Denoting or relating to so!ware or hardware that has been superseded but is
    di"cult to replace because of its wide use.
    ashdavies.dev | [email protected]

    View Slide

  3. Change
    ashdavies.dev | [email protected]

    View Slide

  4. Testing
    ashdavies.dev | [email protected]

    View Slide

  5. Testing: Awareness
    ashdavies.dev | [email protected]

    View Slide

  6. Testing: Architecture
    ashdavies.dev | [email protected]

    View Slide

  7. Testing: Con!dence
    ashdavies.dev | [email protected]

    View Slide

  8. Testing: Documentation
    ashdavies.dev | [email protected]

    View Slide

  9. Testing: Stability
    !
    ashdavies.dev | [email protected]

    View Slide

  10. Testing
    ashdavies.dev | [email protected]

    View Slide

  11. Testing
    • Unit
    • Instrumentation
    • Integration
    • End-to-End
    • Monkey
    • Smoke
    ashdavies.dev | [email protected]

    View Slide

  12. ashdavies.dev | [email protected]

    View Slide

  13. Anti-Pa!erns and Code Smell
    ashdavies.dev | [email protected]

    View Slide

  14. Extreme Programming
    Test-Driven Development
    ashdavies.dev | [email protected]

    View Slide

  15. Debugging is like being the
    detective in a crime movie where
    you are also the murderer.
    — Filipe Fo!es
    ashdavies.dev | [email protected]

    View Slide

  16. Kotlin (noun)
    Awesome.
    ashdavies.dev | [email protected]

    View Slide

  17. Kotlin: Mutability
    Risks of Mutation
    ashdavies.dev | [email protected]

    View Slide

  18. Kotlin: Mutability
    Risks of Mutation
    fun sumAbsolute(list: MutableList): Int {
    for (i in list.indices) list[i] = abs(list[i])
    return list.sum()
    }
    ashdavies.dev | [email protected]

    View Slide

  19. Kotlin: Mutability
    Risks of Mutation
    private val GROUNDHOG_DAY = TODO("java.util.Date()")
    fun sta!OfSpring(): java.util.Date = GROUNDHOG_DAY
    val pa!yDate = sta!OfSpring()
    pa!yDate.month = pa!yDate.month + 1
    // Date is mutable
    !
    ashdavies.dev | [email protected]

    View Slide

  20. Kotlin: Mutability
    Shared Mutable State

    ashdavies.dev | [email protected]

    View Slide

  21. Kotlin: Immutability
    Unidirectional Data Flow
    !
    ashdavies.dev | [email protected]

    View Slide

  22. Kotlin: Immutability
    Collections
    fun List.toMutableList(): MutableList
    fun Map.toMutableMap(): MutableMap
    fun Set.toMutableSet(): MutableSet
    ashdavies.dev | [email protected]

    View Slide

  23. Kotlin: Immutability
    IntelliJ IDEA
    ashdavies.dev | [email protected]

    View Slide

  24. Kotlin: Immutability
    Final Concretions
    !
    By default, Kotlin classes are !nal – they can't be inherited
    ashdavies.dev | [email protected]

    View Slide

  25. Kotlin: Immutability
    Final Concretions
    !
    open class Base // Class is open for inheritance
    ashdavies.dev | [email protected]

    View Slide

  26. Kotlin: Immutability
    All-Open Compiler Plugin
    org.jetbrains.kotlin.plugin.allopen
    ashdavies.dev | [email protected]

    View Slide

  27. Anti-Pa!erns and Code Smell
    ashdavies.dev | [email protected]

    View Slide

  28. Refactoring: Dependencies
    ashdavies.dev | [email protected]

    View Slide

  29. Refactoring: Dependencies
    ===================================================================
    di! --git a/Co!eeMaker.kt b/Co!eeMaker.kt
    - internal class Co!eeMaker {
    - private val heater: Heater = ElectricHeater()
    - private val pump: Pump = Thermosiphon(heater)
    - }
    + internal class Co!eeMaker(
    + private val heater: Heater,
    + private val pump: Pump,
    + )
    ashdavies.dev | [email protected]

    View Slide

  30. Refactoring: Dependencies
    ===================================================================
    di! --git a/Co!eeMaker.kt b/Co!eeMaker.kt
    + internal class Co!eeMaker(
    + private val heater: Heater,
    - private val thermosiphon: Thermosiphon,
    + private val pump: Pump,
    + )
    +
    + internal inte"ace Pump {
    + fun pump()
    + }
    +
    - internal class Thermosiphon {
    + internal class Thermosiphon : Pump {
    ashdavies.dev | [email protected]

    View Slide

  31. Testing: Dependencies
    Dependency Injection
    internal class Co!eeMaker(
    private val heater: Heater,
    private val pump: Pump,
    )
    ashdavies.dev | [email protected]

    View Slide

  32. Testing: Dependencies
    Dependency Injection
    val heater = NuclearFusionHeater() // Expensive!
    val maker = Co!eeMaker(
    pump = Thermosiphon(heater),
    heater = heater,
    )
    asse"True(maker.brew())
    ashdavies.dev | [email protected]

    View Slide

  33. Testing: Dependencies
    Dependency Injection
    val heater = DiskCachedHeater() // Stateful!
    val maker = Co!eeMaker(
    pump = Thermosiphon(heater),
    heater = heater,
    )
    asse"True(maker.brew())
    ashdavies.dev | [email protected]

    View Slide

  34. Testing: Dependencies
    Dependency Injection
    val heater = UnbalancedHeater() // Error prone!
    val maker = Co!eeMaker(
    pump = Thermosiphon(heater),
    heater = heater,
    )
    asse"True(maker.brew())
    ashdavies.dev | [email protected]

    View Slide

  35. Testing: Dependencies
    Test Doubles
    ashdavies.dev | [email protected]

    View Slide

  36. Testing: Dependencies
    Mocks
    ashdavies.dev | [email protected]

    View Slide

  37. Testing: Dependencies
    Mockito
    "Tasty mocking framework for unit tests in Java".
    ashdavies.dev | [email protected]

    View Slide

  38. Testing: Mocks
    val heater = mock()
    val pump = mock()
    val maker = Co!eeMaker(
    heater = heater,
    pump = pump,
    )
    asse"True(maker.brew())
    ashdavies.dev | [email protected]

    View Slide

  39. Testing: Mocks
    val heater = mock()
    val pump = mock()
    val maker = Co!eeMaker(
    heater = heater,
    pump = pump,
    )
    asse"True(maker.brew()) // ⾠ Fails!
    ashdavies.dev | [email protected]

    View Slide

  40. Testing: Mocks
    val heater = mock {
    on { isHeating } doAnswer { true }
    }
    val pump = mock {
    on { pump() } doAnswer { true }
    }
    val maker = Co!eeMaker(
    heater = heater,
    pump = pump,
    )
    asse"True(maker.brew())
    ashdavies.dev | [email protected]

    View Slide

  41. Testing: Mocks
    javadoc.io/doc/
    org.mockito/mockito-core/latest/
    org/mockito/Mockito.html
    ashdavies.dev | [email protected]

    View Slide

  42. Testing: Mocks
    Footguns
    !"
    ashdavies.dev | [email protected]

    View Slide

  43. Testing: Mocks
    Accidental Invocation
    val heater = mock {
    on { isHeating } doAnswer { true } // Actual invocation!
    }
    ashdavies.dev | [email protected]

    View Slide

  44. Testing: Mocks
    Accidental Invocation
    spy(emptyList()) {
    on { get(0) } doAnswer { "foo" } // throws IndexOutOfBoundsException
    }
    ashdavies.dev | [email protected]

    View Slide

  45. Testing: Mocks
    API Sensitivity
    internal inte!ace Heater {
    val isHeating: Boolean
    }
    val heater = mock {
    on { isHeating } doAnswer { true }
    }
    ashdavies.dev | [email protected]

    View Slide

  46. Testing: Mocks
    API Sensitivity
    internal inte!ace Heater {
    + fun heat(body: () -> T): T
    val isHeating: Boolean
    }
    val heater = mock {
    on { isHeating } doAnswer { true }
    }
    ashdavies.dev | [email protected]

    View Slide

  47. Testing: Mocks
    API Sensitivity
    internal inte!ace Co"eeDistributor {
    fun announce(vararg name: String): Boolean
    }
    val mockDistributor = mock {
    on { announce(any(), any()) } doReturn true
    }
    val announced = mockDistributor.announce(
    "Steve", "Roger", "Stan",
    )
    asse#True(announced) // False: We only stubbed two names!
    ashdavies.dev | [email protected]

    View Slide

  48. Testing: Mocks
    Default Answers
    val heater: Heater = mock() // No default answer
    val isHeating: Boolean = heater.isHeating // Null
    ashdavies.dev | [email protected]

    View Slide

  49. Testing: Mocks
    Default Answers
    val heater: Heater = mock(defaultAnswer = RETURNS_SMART_NULLS)
    val isHeating: Boolean = heater.isHeating // false
    ashdavies.dev | [email protected]

    View Slide

  50. Testing: Mocks
    Default Answers
    • CALLS_REAL_METHODS
    • RETURNS_DEEP_STUBS
    • RETURNS_DEFAULTS
    • RETURNS_MOCKS
    • RETURNS_SELF
    • RETURNS_SMART_NULLS
    ashdavies.dev | [email protected]

    View Slide

  51. Testing: Mocks
    Type Safety
    public inte!ace OngoingStubbing {
    OngoingStubbing thenAnswer(Answer> answer);
    }
    ashdavies.dev | [email protected]

    View Slide

  52. Testing: Mocks
    Pe!ormance
    Expensive real implementations replaced by expensive mocks.
    • Runtime code generation
    • Bytecode manipulation
    • Re!ection
    !
    ashdavies.dev | [email protected]

    View Slide

  53. Testing: Mocks
    Pe!ormance
    internal class Co!eeMakerTest {
    private lateinit var heater: Heater
    @Before
    fun setUp() {
    heater = mock {
    on { isHeating } doAnswer { true }
    }
    }
    }
    ashdavies.dev | [email protected]

    View Slide

  54. Testing: Mocks
    Dynamic Mutability
    internal class Co!eeMakerTest {
    private lateinit var heater: Heater
    @Before
    fun setUp() {
    heater = mock {
    on { isHeating } doAnswer { true }
    }
    }
    @Test
    fun `should brew co!ee`() {
    // heater already has state!
    }
    }
    ashdavies.dev | [email protected]

    View Slide

  55. Testing: Mocks
    Dynamic Mutability
    Framework generated mocks introduce a
    shared, mutable, dynamic, runtime
    declaration.
    ashdavies.dev | [email protected]

    View Slide

  56. Unpredictability
    Costs
    ashdavies.dev | [email protected]

    View Slide

  57. Unpredictability: Costs
    Learning Curve
    ashdavies.dev | [email protected]

    View Slide

  58. Unpredictability: Costs
    Peer Review
    ashdavies.dev | [email protected]

    View Slide

  59. Unpredictability: Costs
    Risk of Bugs
    ashdavies.dev | [email protected]

    View Slide

  60. Unpredictability: Costs
    Slowed Feature Delivery
    !!!
    ashdavies.dev | [email protected]

    View Slide

  61. Unpredictability
    Victims
    • Junior developers
    • New team members
    • Future you
    ashdavies.dev | [email protected]

    View Slide

  62. Testing: Mocks
    Don't Mock Classes You Don't Own
    testing.googleblog.com/2020/07/testing-on-toilet-dont-mock-types-you.html
    ashdavies.dev | [email protected]

    View Slide

  63. You Don't Own Your Code!
    Your code belongs to your team.
    Be considerate.
    ashdavies.dev | [email protected]

    View Slide

  64. Testing: Mocks
    Considerations
    ashdavies.dev | [email protected]

    View Slide

  65. Testing: Mocks
    Interaction Veri!cation
    !
    ashdavies.dev | [email protected]

    View Slide

  66. Testing: Mocks
    What Now?
    ashdavies.dev | [email protected]

    View Slide

  67. Testing: Stubs
    ashdavies.dev | [email protected]

    View Slide

  68. Testing: Stubs
    Simple
    internal inte!ace Pump {
    fun pump(): Boolean
    }
    internal object StubPump : Pump {
    override fun pump(): Boolean = true
    }
    ashdavies.dev | [email protected]

    View Slide

  69. Testing: Stubs
    Idiomatic
    internal fun inte!ace Pump {
    fun pump(): Boolean
    }
    val stub = Pump { true }
    ashdavies.dev | [email protected]

    View Slide

  70. Testing: Stubs
    API Sensitive
    + private const val DEFAULT_AMOUNT = 250 // ml
    +
    - internal fun inte!ace Pump {
    + internal inte!ace Pump {
    - fun pump(): Boolean
    + fun pump(amount: Int = DEFAULT_AMOUNT): Boolean
    + }
    ashdavies.dev | [email protected]

    View Slide

  71. Testing: Stubs
    API Sensitive
    private const val DEFAULT_AMOUNT = 250 // ml
    internal inte!ace Pump {
    fun pump(amount: Int = DEFAULT_AMOUNT): Boolean
    }
    val stub = Pump { true } // Compilation failure...
    ashdavies.dev | [email protected]

    View Slide

  72. Testing: Fakes
    !
    ashdavies.dev | [email protected]

    View Slide

  73. Testing: Fakes
    public class FakePump(private val onPump: (Boolean) -> Boolean) : Pump {
    public val pumped = mutableListOf>()
    override fun pump(full: Boolean): Boolean = onPump(full).also {
    pumped += full to it
    }
    }
    ashdavies.dev | [email protected]

    View Slide

  74. Testing: Fakes
    Additional Behaviour
    private class DelegatingHeater(
    private val delegate: Heater,
    ) : Heater by delegate {
    private val _drinks = mutableListOf()
    val drinks: List by ::_drinks
    override fun heat(body: () -> T): T {
    return delegate.heat(body).also { _drinks += it }
    }
    }
    ashdavies.dev | [email protected]

    View Slide

  75. Testing: Fakes
    Authoring
    ashdavies.dev | [email protected]

    View Slide

  76. Testing: Fakes
    Responsibility
    ashdavies.dev | [email protected]

    View Slide

  77. Testing: Fakes
    Quali!cations
    Those who wrote the code are the most
    uniquely quali!ed to write the tests.
    ashdavies.dev | [email protected]

    View Slide

  78. Testing: Libraries
    • androidx.compose.ui:ui-test-junit4
    • com.slack.circuit:circuit-test
    • io.ktor:ktor-server-test-host
    • io.ktor:ktor-client-mock
    • kotlinx-coroutines-test
    ashdavies.dev | [email protected]

    View Slide

  79. Testing: In Memory
    internal fun inte!ace Co"eeStore {
    fun has(type: Co"eeType): Boolean
    }
    internal enum class Co"eeType {
    CAPPUCCINO,
    ESPRESSO,
    LATTE,
    }
    ashdavies.dev | [email protected]

    View Slide

  80. Testing: In Memory
    internal class InMemoryCo!eeStore : Co!eeStore {
    private val _stock = mutableMapOf()
    val stock: Map by ::_stock
    override fun has(type: Co!eeType): Boolean {
    return (_stock[type] ?: 0) > 0
    }
    fun add(type: Co!eeType, amount: Int = 1) {
    _stock[type] = (_stock[type] ?: 0) + amount
    }
    }
    ashdavies.dev | [email protected]

    View Slide

  81. Testing: In Memory
    Bonus!
    In-memory implementations for local or network overrides.
    ashdavies.dev | [email protected]

    View Slide

  82. Reality
    ashdavies.dev | [email protected]

    View Slide

  83. Inte!ace Segregation
    Principle
    No code should be forced to depend on
    methods it does not use.
    Image: dribbble.com/shots/3251806-Inte!ace-Segregation-Principle

    View Slide

  84. Anti-Pa!erns and Code Smell
    Refactoring: Seams
    !
    ashdavies.dev | [email protected]

    View Slide

  85. Refactoring: Seams
    !
    Preprocessing
    • Kotlin Symbol Processing
    • Kotlin Compiler Plugins
    ashdavies.dev | [email protected]

    View Slide

  86. Refactoring: Seams
    !
    Linking: Classpath
    impo! "t.Parser
    internal class FitFilter {
    private val parser: Parser =
    Parser.newInstance()
    }
    ashdavies.dev | [email protected]

    View Slide

  87. Refactoring: Seams
    !
    Linking: Classpath
    buildscript {
    dependencies {
    val googleServicesVersion = libs.versions.google.services.get()
    classpath("com.google.gms:google-services:$googleServicesVersion")
    }
    }
    ashdavies.dev | [email protected]

    View Slide

  88. Refactoring: Seams
    !
    Objects
    internal class FitFilter {
    private val parser: Parser =
    Parser.newInstance()
    }
    ashdavies.dev | [email protected]

    View Slide

  89. Refactoring: Seams
    !
    Objects: Refactoring
    ===================================================================
    di! --git a/FitFilter.kt b/FitFilter.kt
    - internal class FitFilter {
    - private val parser: Parser =
    - Parser.newInstance()
    - }
    -
    + internal fun inte"ace FitFilter {
    + fun #lter(input: String): String
    + }
    +
    + internal fun FitFilter(parser: Parser) = FitFilter { input ->
    + parser.parse(input)
    + }
    ashdavies.dev | [email protected]

    View Slide

  90. Everything is an API
    ashdavies.dev/talks/everything-is-an-api-berlin-droidcon/
    Build versatile and scalable applications with
    careful API decisions.
    ashdavies.dev | [email protected]

    View Slide

  91. Testing: Android
    Legacy Inheritance
    ashdavies.dev | [email protected]

    View Slide

  92. android.content.Context
    ashdavies.dev | [email protected]

    View Slide

  93. God Objects
    !
    ashdavies.dev | [email protected]

    View Slide

  94. Ravioli Code
    ! "
    ashdavies.dev | [email protected]

    View Slide

  95. Conclusion
    • Don’t mock classes you don’t own.
    • Don’t mock classes you do own.
    • Don’t mock classes (except Context).
    ashdavies.dev | [email protected]

    View Slide

  96. Every existing thing is born
    without reason, prolongs itself out
    of weakness, and dies by chance.
    — Jean-Paul Sa!re
    ashdavies.dev | [email protected]

    View Slide

  97. Thanks!
    github.com/ashdavies/playground.ashdavies.dev/
    blob/feature/beyond-the-mockery/
    app-launcher/common/src/jvmTest/
    kotlin/io/ashdavies/playground/
    Co!eeMakerTest.kt
    ashdavies.dev | [email protected]

    View Slide

  98. Fu!her Reading
    • Ma!in Flower: Mocks Aren't Stubs
    ma!infowler.com/a!icles/mocksArentStubs.html
    • Ma!in Fowler: Practical Test Pyramid
    ma!infowler.com/a!icles/practical-test-pyramid.html
    • Images: Monkey User
    monkeyuser.com
    • Michael Feathers: Working E"ectively with Legacy Code
    ISBN: 978-0-13117-705-5
    • Steve Freeman, Nat Pryce: Growing Object-Oriented So#ware, Guided by Tests
    ISBN: 978-0-32150-362-6
    • Testing on the Toilet: Don't mock Types You Don't Own
    testing.googleblog.com/2020/07/testing-on-toilet-dont-mock-types-you.html
    • Testing on the Toilet: Know Your Test Doubles
    testing.googleblog.com/2013/07/testing-on-toilet-know-your-test-doubles.html
    ashdavies.dev | [email protected]

    View Slide