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

Droidcon Lisbon: Beyond the Mockery

Ash Davies
September 29, 2023

Droidcon Lisbon: 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

September 29, 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
  2. Anti-Pa!erns and Code Smell My God, what is that smell?

    Oh. — Veronica Corningstone ashdavies.dev
  3. Debugging is like being the detective in a crime movie

    where you are also the murderer. — Filipe Fo!es ashdavies.dev
  4. 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
  5. 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
  6. Kotlin: Immutability Final Concretions ! By default, Kotlin classes are

    !nal – they can't be inherited ashdavies.dev
  7. Refactoring: Seams ! Linking: Classpath impo! "t.Parser internal class FitFilter

    { private val parser: Parser = Parser.newInstance() } ashdavies.dev
  8. Refactoring: Seams ! Linking: Classpath buildscript { dependencies { val

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

    parser: Parser = Parser.newInstance() } ashdavies.dev
  10. 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
  11. Refactoring Dependency Injection =================================================================== 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
  12. Refactoring Dependency Injection =================================================================== 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
  13. Testing: Dependencies Dependency Injection val heater = NuclearFusionHeater() // Expensive!

    val maker = Co!eeMaker( pump = Thermosiphon(heater), heater = heater, ) asse"True(maker.brew()) ashdavies.dev
  14. Testing: Dependencies Dependency Injection val heater = DiskCachedHeater() // Stateful!

    val maker = Co!eeMaker( pump = Thermosiphon(heater), heater = heater, ) asse"True(maker.brew()) ashdavies.dev
  15. Testing: Dependencies Dependency Injection val heater = UnbalancedHeater() // Error

    prone! val maker = Co!eeMaker( pump = Thermosiphon(heater), heater = heater, ) asse"True(maker.brew()) ashdavies.dev
  16. Testing: Mocks val heater = mock<Heater>() val pump = mock<Pump>()

    val maker = Co!eeMaker( heater = heater, pump = pump, ) asse"True(maker.brew()) ashdavies.dev
  17. 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
  18. 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
  19. Testing: Mocks Accidental Invocation val heater = mock<Heater> { on

    { isHeating } doAnswer { true } // Actual invocation! } ashdavies.dev
  20. Testing: Mocks Accidental Invocation spy(emptyList<String>()) { on { get(0) }

    doAnswer { "foo" } // throws IndexOutOfBoundsException } ashdavies.dev
  21. Testing: Mocks API Sensitivity internal inte!ace Heater { val isHeating:

    Boolean } val heater = mock<Heater> { on { isHeating } doAnswer { true } } ashdavies.dev
  22. 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
  23. 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
  24. Testing: Mocks Default Answers val heater: Heater = mock() //

    No default answer val isHeating: Boolean = heater.isHeating // Null ashdavies.dev
  25. Testing: Mocks Default Answers val heater: Heater = mock(defaultAnswer =

    RETURNS_SMART_NULLS) val isHeating: Boolean = heater.isHeating // false ashdavies.dev
  26. Testing: Mocks Default Answers • CALLS_REAL_METHODS • RETURNS_DEEP_STUBS • RETURNS_DEFAULTS

    • RETURNS_MOCKS • RETURNS_SELF • RETURNS_SMART_NULLS ashdavies.dev
  27. Testing: Mocks Pe!ormance Expensive real implementations replaced by expensive mocks.

    • Runtime code generation • Bytecode manipulation • Re!ection ! ashdavies.dev
  28. Testing: Mocks Pe!ormance internal class Co!eeMakerTest { private lateinit var

    heater: Heater @Before fun setUp() { heater = mock { on { isHeating } doAnswer { true } } } } ashdavies.dev
  29. 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
  30. Testing: Mocks Dynamic Mutability Framework generated mocks introduce a shared,

    mutable, dynamic, runtime declaration. ashdavies.dev
  31. You Don't Own Your Code! Your code belongs to your

    team. Be considerate. ashdavies.dev
  32. Testing: Stubs Simple internal inte!ace Pump { fun pump(): Boolean

    } internal object StubPump : Pump { override fun pump(): Boolean = true } ashdavies.dev
  33. Testing: Stubs Idiomatic internal fun inte!ace Pump { fun pump():

    Boolean } val stub = Pump { true } ashdavies.dev
  34. 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
  35. 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
  36. 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
  37. 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
  38. Testing: Fakes Quali!cations Those who wrote the code are the

    most uniquely quali!ed to write the tests. ashdavies.dev
  39. 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
  40. 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
  41. 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
  42. Conclusion • Don’t mock classes you don’t own. • Don’t

    mock classes you do own. • Don’t mock classes. • Except Context. ashdavies.dev
  43. Every existing thing is born without reason, prolongs itself out

    of weakness, and dies by chance. — Jean-Paul Sa!re ashdavies.dev
  44. 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 • Sam Edwards: Wrapping Mockito Mocks for Reusability handstandsam.com/2020/06/08/wrapping-mockito-mocks-for-reusability • 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 • Marcello Galhardo: No Mocks Allowed marcellogalhardo.dev/posts/no-mocks-allowed ashdavies.dev