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 as a black box hard?

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)
     
    verify {
        persistenceService.saveObject(match {
            it.someProperty == 345
        })
    }
    
}

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

how does this apply to my project?

phake.readthedocs.io

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

How to test complex software with mocks?

By Mattia Tommasone

How to test complex software with mocks?

  • 172