Extreme mocking

Mattia Tommasone

@raibaz

About

  • 10+ years spent writing (and reading) code
  • FLOSS Enthusiast
  • Former:
    • Developer Advocate @ Google
    • Kids' football coach 
    • University lecturer
    • Conference speaker
  • Current:
    • Lead developer @ Brandon Group

axiom 1: tests are vital to every software project

Axiom 1.1: every component in a software project should be described by tests

Disclaimer #1

class Adder {
    fun add(op1: Int, op2: Int) {
        return op1 + op2
    }
}

however, in the real world...

  • Database connections
  • HTTP connections
  • External services
  • Third party library dependencies
  • Non-trivial component interactions
  • Side effects
class BusinessLogicService(
    private val persistenceService: MyPersistenceService
) {
    fun doSomethingRelevant(objectId: Long, someValue: Int) {
        val myObject = persistenceService.retrieveObject(objectId)
            ?: throw IllegalArgumentException("$objectId not found")
        
        myObject.someProperty = someValue
        
        persistenceService.saveObject(myObject)
    }
}
  • You may not have a place to persists objects to
  • Making sure objects are stored and retrieved correctly is outside of the scope of this component

why is testing this component hard?

well, then maybe i can change my component

class BusinessLogicService(
    private val persistenceService: MyPersistenceService
) {
    fun doSomethingRelevant(myObject: MyObjectClass, someValue: Int) {
        
        myObject.someProperty = someValue
        
        return myObject 
        // And leave it to someone else to care about persistence
    }
}

but this is just moving the problem somewhere else

not really addressing it

  • You may not necessarily be able to change the semantics of a component
  • Someone (you) will still have to make sure the persistence layer and the business logic interact properly

why is this not a solution?

class BusinessLogicService(
    private val persistenceService: MyPersistenceService
) {
    fun doSomethingRelevant(objectId: Long, someValue: Int) {
        val myObject = persistenceService.retrieveObject(objectId)
            ?: throw IllegalArgumentException("$objectId not found")
        
        myObject.someProperty = someValue
        
        persistenceService.saveObject(myObject)
    }
}

what exactly do we want to test here?

  • Error conditions:
    • Calling this method on a non-existing object should fail
  • The "happy path": this component updates the requested object with the requested value

problem statement

  • We need a way to drive the behavior of the persistence layer
  • We need a way to perform assertions on how our component interacts with the persistence layer

disclaimer #2

mockk.io

class BusinessLogicServiceTest {
    @MockK
    private lateinit var persistenceService : MyPersistenceService
    
    private lateinit var service : BusinessLogicService
    
    @Before
    fun setup() {
        MockKAnnotations.init(this)
        
        service = BusinessLogicService(persistenceService)
    }
    
    @Test
    fun `doing something with a non-existing object throws an exception`() {
        every {
            persistenceService.retrieveObject(any())
        } returns null
        
        assertFailsWith<IllegalArgumentException> {
            service.doSomethingRelevant(123L, 345)
        }
    }
}
@Test
fun `doing something with an existing object updates it and saves it`() {
    every {
        persistenceService.retrieveObject(any())
    } returns MyClass()

    service.doSomethingRelevant(123L, 345)
    
    verify {
        persistenceService.saveObject(any())
    }
}

How do we test that the value being saved has actually been updated?

@Test
fun `doing something with an existing object updates it and saves it`() {
    every {
        persistenceService.retrieveObject(any())
    } returns MyClass()

    service.doSomethingRelevant(123L, 345)
    
    val slot = slot<MyClass>()
    verify {
        persistenceService.saveObject(capture(slot))
    }
    
    assertEquals(345, slot.captured.someProperty)
}

Some glossary 

  • Strict mocking vs. non-strict (or relaxed) mocking
  • Mocks vs. Stubs vs. Spies
  • Argument matching

why exactly is this so powerful?

we can test interactions, i.e. contracts between components

we can test logic that interacts with stuff that may not be available in a CI environment

mocking dependencies can help expose bad design

It helps obtain command-query separation

it's not only about object-oriented code

how does this apply to my project?

phake.readthedocs.io

docs.python.org/3/library/unittest.mock.html

Extreme Mocking

By Mattia Tommasone

Extreme Mocking

  • 246