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

Side-effects within the Business Logic core

Side-effects within the Business Logic core

Testing is hard. When it comes to separate concerns around I/O, we often end up writing lot of test code that needs to be continuously updated for a long time. At some point we all asked ourselves: “If my business logic code is correct, why I have to keep updating mocks and stubs so often? Am I focusing on adding business value or just gardening test code and mock/stub objects?”

This talk will explore and present few scenarios where it seems we stopped asking ourselves: “What is the problem we are really trying to solve here? Would everything get easier if we try to adjust the boundaries slightly?”

2fa9df92d8d7eba3e17207c86f953be3?s=128

Filippo Vitale

September 20, 2019
Tweet

Transcript

  1. @filippovitale September 2019 Testing-with-Mock: hold my beer... Side-effects within the

    Business Logic Core
  2. The opinions expressed in this presentation are those of the

    author. They do not purport to reflect the opinions or views of NETSCOUT or its employees.
  3. ➔ 165+ service providers (Global) ➔ 66% Fortune 1000 Enterprise

  4. ASERT – https://twitter.com/ASERTResearch

  5. None
  6. Happy Land

  7. Happy Land

  8. A f B

  9. Testing - Example-based - Mutation - Property-based

  10. Testing - Example-based - Mutation - Property-based Here, we don’t

    need mocking objects
  11. Mocking Objects are widely used An Empirical Study on the

    Usage of Mocking Frameworks in Software Testing, Shaikh Mostafa, Xiaoyin Wang (2014) - 23% are using a mocking framework - Small portion of the dependencies
  12. What dependencies get mocked? To Mock or Not To Mock?

    An Empirical Study on Mocking Practices (2017) Davide Spadini, Maurício Aniche - Mix of OS and enterprise (big) projects - 2,000+ mocking objects usage analyzed - 100+ devs surveys and interviews
  13. What dependencies get mocked? 70% 36% 6%

  14. What dependencies get mocked? 70% 36% 6% “Classes related to

    external resources are often mocked due to their complex setup and slowness” External Dependencies, DBs, Web Services
  15. What dependencies get mocked? 70% 36% 6% External Dependencies, DBs,

    Web Services
  16. What dependencies get mocked? 70% 36% 6% Business Logic (Domain)

    External Dependencies, DBs, Web Services “Classes related to Business logic are mocked only when they are too complex”
  17. What dependencies get mocked? 70% 36% 6% Business Logic (Domain)

    Language Libs External Dependencies, DBs, Web Services To Mock or Not To Mock? An Empirical Study on Mocking Practices (2017) Davide Spadini, Maurício Aniche
  18. Why sometimes we feel mocking objects seems the only way

    to test the Business Logic?
  19. Test Double https://martinfowler.com/bliki/TestDouble.html – http://xunitpatterns.com/Test%20Double.html Martin Fowler

  20. Test Double Mock Object https://en.wikipedia.org/wiki/Mock_object

  21. Test Double Mock Object Stubs Mocks Fakes

  22. Test Double Mock Object Stubs Mocks Fakes

  23. Test Double Mock Object Stubs Mocks Fakes https://martinfowler.com/bliki/TestDouble.html

  24. Common Scenario: “Huge dependencies that are too hard to create”

  25. http://xunitpatterns.com/Test%20Stub.html “We can use a Test Stub [...] to control

    the behavior of the SUT with various indirect inputs”
  26. class PotatoService(config: Config) { val potatoVariety: String = config.getPotatoVariety val

    maxPotatoes: Int = config.getMaxPotatoes def makePotatoSalad(): Salad = ??? } Imagine a world without mocks, Ken Scambler
  27. "PotatoService" should "make an expectedSalad" in { val configStub =

    mock[Config] (configStub.getPotatoVariety _).expects().returning("pontiac") (configStub.getMaxPotatoes _).expects().returning(33) val service = new PotatoService(configStub) service.makePotatoSalad() should equal (expectedSalad) }
  28. https://www.amazon.com/Wenger-16999-Swiss-Knife-Giant/dp/B001DZTJRQ

  29. “Know as little as you need” (to produce as little

    as you can)
  30. class PotatoService(variety: String, potatoes: Int) { def makePotatoSalad(): Salad =

    ??? }
  31. class PotatoService(variety: String, potatoes: Int) { def makePotatoSalad(): Salad =

    ??? } val appConfig: Config = readFromFile(appConfigFilename) val service = new PotatoService(appConfig.getPotatoVariety, appConfig.getMaxPotatoes)
  32. "PotatoService" should "make an expectedSalad" in { val service =

    new PotatoService("pontiac", 33) service.makePotatoSalad() should equal (expectedSalad) }
  33. Common Scenario: “Fragile mutable domain modelling”

  34. http://xunitpatterns.com/Mock%20Object.html “We can use a Mock as an observation point

    [...] caused by an inability to observe side-effects of invoking methods on the SUT”
  35. “Tell, don’t Ask” https://martinfowler.com/bliki/TellDontAsk.html

  36. trait Customer { def buyDrink(): Unit } trait Wallet {

    def removeCoins(amount: Int): Int def getAmount: Int } trait VendingMachine { def insertCoins(amount: Int): Unit def collectCan(): Can def getStoredCash: Int } Imagine a world without mocks, Ken Scambler
  37. "A Customer" should "collect aCan inserting 3 coins" in {

    val walletStub = mock[Wallet] (walletStub.removeCoins _).expects(3).returning(3) .once() val machineStub = mock[VendingMachine] (machineStub.insertCoins _).expects(3).returning() .once() (machineStub.collectCan _).expects().returning(aCan) .once() val customer = new RealCustomer(...) customer.buyDrink() }
  38. trait Customer { def buyDrink(vm: VendingMachine): (Customer, VendingMachine) } trait

    Wallet { def removeCoins(amount: Int): Wallet def getAmount: Int } trait VendingMachine { def insertCoins(amount: Int): VendingMachine def collectCan(): (Option[Can], VendingMachine) def getStoredCash: Int } Imagine a world without mocks, Ken Scambler
  39. "A Customer" should "collect a can inserting 3 coins" in

    { val wallet = new Wallet(10) val customer = new Customer(wallet, 0) // can held val machine = new VendingMachine(3, false) // price, paid val (poorCustomer, richMachine) = c.buyDrink(m) poorCustomer.getWallet.getAmount should equal (10 - 3) poorCustomer.hasCan should be (true) }
  40. None
  41. "A Customer" should "collect a can inserting 3 coins" in

    { val wallet = new Wallet(10) val customer = new Customer(wallet, 0) // can held val machine = new VendingMachine(3, false) // price, paid val (poorCustomer, richMachine) = c.buyDrink(m) poorCustomer.getWallet.getAmount should equal (10 - 3) poorCustomer.hasCan should be (true) } But then it’s an Integration Test! val ab: A => B = ... val bc: B => C = ... val cd: C => D = ... val de: D => E = ... val ae: A => E = ab andThen bc andThen cd andThen de
  42. @macu_ic – https://unsplash.com/photos/fQWKGEllX2c FP is a tall tree

  43. Common Scenario: “Essential Effects or I/O coupled with pure Logic”

  44. case class Malware(name: String, hasSample: Boolean) case class Indicator(malware: Malware,

    hashes: List[SHA256Hash]) val hakai = Indicator(Malware("Hakai DDoS botnet based off the Gafgyt family of IoT malware", hasSample = true), List("d96...88f", "2d7...c98")) ASERT Team, https://www.netscout.com/blog/asert/realtek-sdk-exploits-rise-egypt
  45. def fetchMalware(id: MalwareID): Malware = { } def fetchSampleHashes(id: MalwareID):

    List[String] = { } def getMalwareIndicator(malwareId: MalwareID): Indicator = { val malware = fetchMalware(malwareId) if (malware.hasSample) { Indicator(malware, fetchSampleHashes(malwareId)) } else { Indicator(malware, List.empty) } }
  46. None
  47. val m = mock[MalwareService] val s = mock[SampleService] (m.fetchMalware _).expects(123).returning(expectedMalware)

    .once (s.fetchSampleHash _).expects(123).returning(expectedHashes).once val i = new MalwareIndicator(m, s) i.getMalwareIndicator(123) should be(expectedIndicator) 70% External Dependencies, DBs, Web Services
  48. val m = mock[MalwareService] val s = mock[SampleService] (m.fetchMalware _).expects(123).returning(expectedMalware)

    .once (s.fetchSampleHash _).expects(123).returning(expectedHashes).once val i = new MalwareIndicator(m, s) i.getMalwareIndicator(123) should be(expectedIndicator)
  49. def fetchMalware(id: MalwareID): Malware = { } def fetchSampleHashes(id: MalwareID):

    List[String] = { } def getMalwareIndicator(malwareId: MalwareID): Indicator = { val malware = fetchMalware(malwareId) if (malware.hasSample) { Indicator(malware, fetchSampleHashes(malwareId)) } else { Indicator(malware, List.empty) } }
  50. Logic and Effects are entangled

  51. @yungnoma , https://unsplash.com/photos/qAShc5SV83M Programs combine Logic & Effects

  52. Tests for: Logic Effects Hard Easy

  53. Hard Easy Few Tests Many Tests Tests for: Logic Effects

  54. Hard Easy Few Tests Many Tests

  55. Hard Easy Few Tests Many Tests

  56. Hard Easy Few Tests Many Tests Test Doubles Are A

    Scam, Matt Diephouse
  57. - Testing Logic+Effects ⇒ possible - Legitimize the Design -

    No motivation on “pushing effects out” - Integration Test ⇒ Unit Test Mocking Objects give you
  58. Fog of War @we_the_royal https://unsplash.com/photos/Cju-BkSkM1k

  59. Mock objects for testing java systems, Why and how developers

    use them, and how they evolve (2018) Davide Spadini 83% Mocks introduced from the beginning Later “Mocks are added at test creation and tend to stay in the test for its lifetime” 87% Mocks never removed from the tests
  60. Mocking Objects are hard to maintain Mock objects for testing

    java systems, Why and how developers use them, and how they evolve (2018) Davide Spadini “Lines involving mocks change often” - Code changes impact the mocks - Mocks increase the coupling test-code - Maintaining the mocks is hard
  61. Separation of Logic and Effects

  62. Don’t commit too early to a specific effect monad!

  63. https://github.com/cutculus/not-a-blog/blob/master/opinionated-haskell-guide-2019.md

  64. The Cost of Abstraction

  65. The Cost of Abstraction “Given a choice of solutions, pick

    the least powerful solution capable of solving your problem” The Rule of Least Power, https://www.w3.org/2001/tag/doc/leastPower.html
  66. “Functional Core, Imperative Shell”

  67. Gary Bernhardt (2012) – https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell

  68. Ports and Adapters (2008), Alistair Cockburn – diagram by Nat

    Pryce Clean Architecture Hexagonal Architecture Onion Architecture Boundaries!
  69. “Branching” minimized in the shell (less “ifs”)

  70. Imperative Shell ⇒ few Integration tests Functional Core ⇒ many

    unit tests
  71. // Functional Core def sampleAvailable(malware: Malware): Either[String, Unit] = if

    (malware.hasSample) Right(()) else Left("No Samples available")
  72. // Functional Core def sampleAvailable(malware: Malware): Either[String, Unit] = if

    (malware.hasSample) Right(()) else Left("No Samples available") // Imperative Shell def getMalwareIndicator(id: MalwareID): Task[Indicator] = for { malware <- fetchMalware(id) hashes <- sampleAvailable(malware) .fold(_ => Task.now(List.empty[SHA256Hash]), _ => fetchSampleHashes(id)) } yield Indicator(malware, hashes)
  73. The Complexity Trap, Daniel Westheide – https://danielwestheide.com/blog/2018/12/07/the-complexity-trap.html // Functional Core

    def sampleAvailable(malware: Malware): Either[String, Unit] = if (malware.hasSample) Right(()) else Left("No Samples available") // Imperative Shell def get...[F[_]: Monad](id: MalwareID): F[Indicator] = for { malware <- fetchMalwareF(id) hashes <- sampleAvailable(malware) .fold(_ => Monad[F].pure(List.empty[SHA256Hash]), _ => fetchSampleHashesF(id)) } yield Indicator(malware, hashes)
  74. “We can combine effectful computation and effect-free ones without making

    them pollute each other, the type system keeps them apart” Nirvana, Simon Peyton Jones – https://youtu.be/iSmkqocn0oQ Simon Peyton Jones
  75. https://github.com/lampepfl/dotty/issues/4712 @pure / @impure

  76. https://www.reddit.com/r/scala/comments/crsuvp/scala_pure_annotation/ @pure / @impure

  77. https://github.com/lampepfl/dotty/issues/4712#issuecomment-403218174 @pure / @impure

  78. Integration tests

  79. Test - TestContainer-Scala - ... Integration Tests CI - Docker

    Compose - ...
  80. - Find discrepancies between current implementation and model - Effects

    vs. Side-Effects State-based property testing
  81. https://en.wikipedia.org/wiki/Don%27t_throw_the_baby_out_with_the_bathwater

  82. Stubs, Mocks and Fakes: The Good Parts™

  83. https://www.flickr.com/photos/nathansmith/4704268314

  84. The Good Parts™ - Fakes for performance tests - Fake

    expensive RealWorld™ things - Concurrency bugs hard to replicate - Codebase with no tests / hard to test - Disposable tests (e.g. when TDD)
  85. Stop Mocking, Start Testing – https://www.youtube.com/watch?v=Xu5EhKVZdV8 “When choosing between Zero

    and One Mock, Try Zero”
  86. Atomic Habits – https://jamesclear.com/common-mental-errors

  87. @filippovitale September 2019 https://commons.wikimedia.org/wiki/File:Tortellini_Bolognesi.jpg