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