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. 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]
  2. Debugging is like being the detective in a crime movie

    where you are also the murderer. — Filipe Fo!es ashdavies.dev | [email protected]
  3. Kotlin: Mutability Risks of Mutation fun sumAbsolute(list: MutableList<Int>): Int {

    for (i in list.indices) list[i] = abs(list[i]) return list.sum() } ashdavies.dev | [email protected]
  4. 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]
  5. 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]
  6. 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]
  7. 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]
  8. 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]
  9. 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]
  10. Testing: Mocks val heater = mock<Heater>() val pump = mock<Pump>()

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

    val maker = Co!eeMaker( heater = heater, pump = pump, ) asse"True(maker.brew()) // ⾠ Fails! ashdavies.dev | [email protected]
  12. Testing: Mocks val heater = mock<Heater> { on { isHeating

    } doAnswer { true } } val pump = mock<Pump> { on { pump() } doAnswer { true } } val maker = Co!eeMaker( heater = heater, pump = pump, ) asse"True(maker.brew()) ashdavies.dev | [email protected]
  13. Testing: Mocks Accidental Invocation val heater = mock<Heater> { on

    { isHeating } doAnswer { true } // Actual invocation! } ashdavies.dev | [email protected]
  14. Testing: Mocks Accidental Invocation spy(emptyList<String>()) { on { get(0) }

    doAnswer { "foo" } // throws IndexOutOfBoundsException } ashdavies.dev | [email protected]
  15. Testing: Mocks API Sensitivity internal inte!ace Heater { val isHeating:

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

    <T : Any> heat(body: () -> T): T val isHeating: Boolean } val heater = mock<Heater> { on { isHeating } doAnswer { true } } ashdavies.dev | [email protected]
  17. Testing: Mocks API Sensitivity internal inte!ace Co"eeDistributor { fun announce(vararg

    name: String): Boolean } val mockDistributor = mock<Co"eeDistributor> { 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]
  18. Testing: Mocks Default Answers val heater: Heater = mock() //

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

    RETURNS_SMART_NULLS) val isHeating: Boolean = heater.isHeating // false ashdavies.dev | [email protected]
  20. Testing: Mocks Default Answers • CALLS_REAL_METHODS • RETURNS_DEEP_STUBS • RETURNS_DEFAULTS

    • RETURNS_MOCKS • RETURNS_SELF • RETURNS_SMART_NULLS ashdavies.dev | [email protected]
  21. Testing: Mocks Pe!ormance Expensive real implementations replaced by expensive mocks.

    • Runtime code generation • Bytecode manipulation • Re!ection ! ashdavies.dev | [email protected]
  22. 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]
  23. 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]
  24. Testing: Stubs Simple internal inte!ace Pump { fun pump(): Boolean

    } internal object StubPump : Pump { override fun pump(): Boolean = true } ashdavies.dev | [email protected]
  25. 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]
  26. 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]
  27. Testing: Fakes public class FakePump(private val onPump: (Boolean) -> Boolean)

    : Pump { public val pumped = mutableListOf<Pair<Boolean, Boolean>>() override fun pump(full: Boolean): Boolean = onPump(full).also { pumped += full to it } } ashdavies.dev | [email protected]
  28. Testing: Fakes Additional Behaviour private class DelegatingHeater( private val delegate:

    Heater, ) : Heater by delegate { private val _drinks = mutableListOf<Any>() val drinks: List<Any> by ::_drinks override fun <T : Any> heat(body: () -> T): T { return delegate.heat(body).also { _drinks += it } } } ashdavies.dev | [email protected]
  29. Testing: Fakes Quali!cations Those who wrote the code are the

    most uniquely quali!ed to write the tests. ashdavies.dev | [email protected]
  30. 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]
  31. Testing: In Memory internal class InMemoryCo!eeStore : Co!eeStore { private

    val _stock = mutableMapOf<Co!eeType, Int>() val stock: Map<Co!eeType, Int> 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]
  32. 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
  33. Refactoring: Seams ! Linking: Classpath impo! "t.Parser internal class FitFilter

    { private val parser: Parser = Parser.newInstance() } ashdavies.dev | [email protected]
  34. Refactoring: Seams ! Linking: Classpath buildscript { dependencies { val

    googleServicesVersion = libs.versions.google.services.get() classpath("com.google.gms:google-services:$googleServicesVersion") } } ashdavies.dev | [email protected]
  35. Refactoring: Seams ! Objects internal class FitFilter { private val

    parser: Parser = Parser.newInstance() } ashdavies.dev | [email protected]
  36. 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]
  37. 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]
  38. Every existing thing is born without reason, prolongs itself out

    of weakness, and dies by chance. — Jean-Paul Sa!re ashdavies.dev | [email protected]
  39. 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]