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?”

Filippo Vitale

September 20, 2019
Tweet

More Decks by Filippo Vitale

Other Decks in Programming

Transcript

  1. 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.
  2. 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
  3. 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
  4. 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
  5. 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”
  6. 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
  7. http://xunitpatterns.com/Test%20Stub.html “We can use a Test Stub [...] to control

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

    maxPotatoes: Int = config.getMaxPotatoes def makePotatoSalad(): Salad = ??? } Imagine a world without mocks, Ken Scambler
  9. "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) }
  10. class PotatoService(variety: String, potatoes: Int) { def makePotatoSalad(): Salad =

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

    new PotatoService("pontiac", 33) service.makePotatoSalad() should equal (expectedSalad) }
  12. 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”
  13. 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
  14. "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() }
  15. 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
  16. "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) }
  17. "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
  18. 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
  19. 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) } }
  20. 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
  21. 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)
  22. 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) } }
  23. - Testing Logic+Effects ⇒ possible - Legitimize the Design -

    No motivation on “pushing effects out” - Integration Test ⇒ Unit Test Mocking Objects give you
  24. 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
  25. 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
  26. 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
  27. Ports and Adapters (2008), Alistair Cockburn – diagram by Nat

    Pryce Clean Architecture Hexagonal Architecture Onion Architecture Boundaries!
  28. // Functional Core def sampleAvailable(malware: Malware): Either[String, Unit] = if

    (malware.hasSample) Right(()) else Left("No Samples available")
  29. // 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)
  30. 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)
  31. “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
  32. - Find discrepancies between current implementation and model - Effects

    vs. Side-Effects State-based property testing
  33. 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)